This is an automated email from the ASF dual-hosted git repository.

xiaoyu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 7ce81a132 [ISSUE #3221] record operation logs in admin (#3328)
7ce81a132 is described below

commit 7ce81a13257a9685b388924ab60a963fd3bcc49e
Author: likeguo <[email protected]>
AuthorDate: Fri Apr 29 14:55:39 2022 +0800

    [ISSUE #3221] record operation logs in admin (#3328)
    
    * rule and selector page list api
    
    * rule and selector page list api
    
    * feature/record-log
    
    * feature/record-log
    
    * feature/record-log
    
    * feature/record-log:add upgrade and pg support
    
    * feature/record-log:add upgrade and pg support
    
    * fixbug/pg script error
    
    * fixbug/pg script error
---
 script/2.4.3-upgrade-2.5.0-mysql.sql               |  16 +-
 script/2.4.3-upgrade-2.5.0-pg.sql                  |  26 ++-
 ...-debug-operation-record-log-controller-api.http |  25 +++
 .../config/properties/DashboardProperties.java     |  54 ++++++
 .../controller/OperationRecordLogController.java   |  54 ++++++
 .../RecordLogDataChangedAdapterListener.java       |  53 ++++++
 .../admin/mapper/OperationRecordLogMapper.java     |  47 ++++++
 .../admin/model/entity/OperationRecordLog.java     | 179 ++++++++++++++++++++
 .../shenyu/admin/model/enums/EventTypeEnum.java    | 124 ++++++++++++++
 .../model/event/AdminDataModelChangedEvent.java    | 188 +++++++++++++++++++++
 .../admin/model/event/BatchChangedEvent.java       |  68 ++++++++
 .../admin/model/event/BatchPluginChangedEvent.java |  57 +++++++
 .../admin/model/event/PluginChangedEvent.java      |  84 +++++++++
 .../admin/service/OperationRecordLogService.java   |  35 ++++
 .../impl/OperationRecordLogServiceImpl.java        |  48 ++++++
 .../admin/service/impl/PluginServiceImpl.java      |  38 ++---
 .../AdminDataModelChangedEventPublisher.java       |  95 +++++++++++
 .../service/publish/PluginEventPublisher.java      | 119 +++++++++++++
 .../org/apache/shenyu/admin/utils/SessionUtil.java |  92 ++++++++++
 .../mappers/operation-record-log-sqlmap.xml        |  57 +++++++
 .../src/main/resources/sql-script/h2/schema.sql    |  12 ++
 .../src/main/resources/sql-script/mysql/schema.sql |  12 ++
 .../src/main/resources/sql-script/pg/schema.sql    |  29 ++++
 .../shenyu/admin/service/PluginServiceTest.java    |   8 +-
 24 files changed, 1493 insertions(+), 27 deletions(-)

diff --git a/script/2.4.3-upgrade-2.5.0-mysql.sql 
b/script/2.4.3-upgrade-2.5.0-mysql.sql
index 7a3fadb3c..e4b59ba2b 100644
--- a/script/2.4.3-upgrade-2.5.0-mysql.sql
+++ b/script/2.4.3-upgrade-2.5.0-mysql.sql
@@ -37,4 +37,18 @@ INSERT IGNORE INTO shenyu_dict (`id`, `type`, `dict_code`, 
`dict_name`, `dict_va
 INSERT IGNORE INTO shenyu_dict (`id`, `type`, `dict_code`, `dict_name`, 
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1516043495265869824', 
'operator', 'OPERATOR', 'endsWith', 'endsWith', 'endsWith', 8, 1);
 
 -- refactor logging name
-UPDATE plugin SET name = 'LoggingConsole' WHERE name = 'logging';
\ No newline at end of file
+UPDATE plugin SET name = 'LoggingConsole' WHERE name = 'logging';
+
+-- new table operation_record_log
+-- ----------------------------
+-- Table structure for operation_record_log
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `operation_record_log`
+(
+    `id`             bigint auto_increment        not null comment 'id' 
primary key,
+    `color`          varchar(20)                  not null comment 'log color',
+    `context`        text                         not null comment 'log 
context',
+    `operator`       varchar(200)                 not null comment 'operator 
[user or app]]',
+    `operation_time` datetime    default now()    not null comment 'operation 
time',
+    `operation_type` varchar(60) default 'update' not null comment 'operation 
type:create/update/delete/register...'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 
ROW_FORMAT=DYNAMIC  comment 'operation record log';
diff --git a/script/2.4.3-upgrade-2.5.0-pg.sql 
b/script/2.4.3-upgrade-2.5.0-pg.sql
index 14eb23462..4997f1088 100644
--- a/script/2.4.3-upgrade-2.5.0-pg.sql
+++ b/script/2.4.3-upgrade-2.5.0-pg.sql
@@ -37,4 +37,28 @@ INSERT INTO shenyu_dict ("id", "type", "dict_code", 
"dict_name", "dict_value", "
 INSERT INTO shenyu_dict ("id", "type", "dict_code", "dict_name", "dict_value", 
"desc", "sort", "enabled") VALUES ('1516043495265869824', 'operator', 
'OPERATOR', 'endsWith', 'endsWith', 'endsWith', 8, 1);
 
 -- refactor logging name
-UPDATE plugin SET name = 'LoggingConsole' WHERE name = 'logging';
+UPDATE plugin SET name = "LoggingConsole" WHERE name = "logging";
+
+-- new table operation_record_log
+-- ----------------------------
+-- Table structure for operation_record_log
+-- ----------------------------
+CREATE TABLE "operation_record_log"
+(
+    "id"             int8                                        NOT NULL,
+    "color"          varchar(20) COLLATE "pg_catalog"."default"  NOT NULL,
+    "context"        text COLLATE "pg_catalog"."default"         NOT NULL,
+    "operator"       varchar(200) COLLATE "pg_catalog"."default" NOT NULL,
+    "operation_time" timestamp(6)                                NOT NULL,
+    "operation_type" varchar(60) COLLATE "pg_catalog"."default"  NOT NULL,
+    CONSTRAINT "operation_record_log_pkey" PRIMARY KEY ("id")
+)
+;
+
+COMMENT ON COLUMN "operation_record_log"."id" IS 'id';
+COMMENT ON COLUMN "operation_record_log"."color" IS 'log color';
+COMMENT ON COLUMN "operation_record_log"."context" IS 'log context';
+COMMENT ON COLUMN "operation_record_log"."operator" IS 'operator [user or 
app]]';
+COMMENT ON COLUMN "operation_record_log"."operation_time" IS 'operation time';
+COMMENT ON COLUMN "operation_record_log"."operation_type" IS 'operation 
type:create/update/delete/register...';
+COMMENT ON TABLE "operation_record_log" IS 'operation record log';
diff --git 
a/shenyu-admin/src/http/http-debug-operation-record-log-controller-api.http 
b/shenyu-admin/src/http/http-debug-operation-record-log-controller-api.http
new file mode 100644
index 000000000..9ea6e181f
--- /dev/null
+++ b/shenyu-admin/src/http/http-debug-operation-record-log-controller-api.http
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# if you debug api, replace your own token
+
+### list
+GET http://localhost:9095/operation-record/log/list
+Accept: application/json
+Content-Type: application/json
+X-Access-Token: 
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNjUwNzg3NTY4fQ.YdxPSutcRClyuj76nYhwHJWkkkMzFVZeBfv5V04ybYA
+
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/DashboardProperties.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/DashboardProperties.java
new file mode 100644
index 000000000..13386868b
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/DashboardProperties.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config.properties;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * admin dashboard properties.
+ */
+@Configuration
+@ConfigurationProperties(prefix = "shenyu.dashboard.core")
+public class DashboardProperties {
+    
+    /**
+     * record log limit.
+     */
+    @Value("${shenyu.dashboard.core.record-log-limit:12}")
+    private Integer recordLogLimit;
+    
+    /**
+     * get recordLogLimit.
+     *
+     * @return limit
+     */
+    public Integer getRecordLogLimit() {
+        return recordLogLimit;
+    }
+    
+    /**
+     * set recordLogLimit.
+     *
+     * @param recordLogLimit limit
+     */
+    public void setRecordLogLimit(final Integer recordLogLimit) {
+        this.recordLogLimit = recordLogLimit;
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/OperationRecordLogController.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/OperationRecordLogController.java
new file mode 100644
index 000000000..cfd411550
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/OperationRecordLogController.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.controller;
+
+import org.apache.shenyu.admin.model.entity.OperationRecordLog;
+import org.apache.shenyu.admin.model.result.AdminResult;
+import org.apache.shenyu.admin.service.OperationRecordLogService;
+import org.apache.shenyu.admin.utils.ResultUtil;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * OperationRecordLogController.
+ */
+@Validated
+@RestController
+@RequestMapping("/operation-record/log")
+public class OperationRecordLogController {
+    
+    private final OperationRecordLogService recordLogService;
+    
+    public OperationRecordLogController(final OperationRecordLogService 
recordLogService) {
+        this.recordLogService = recordLogService;
+    }
+    
+    /**
+     * list.
+     *
+     * @return list
+     */
+    @GetMapping("/list")
+    public AdminResult<List<OperationRecordLog>> list() {
+        return ResultUtil.ok(recordLogService.list());
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/RecordLogDataChangedAdapterListener.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/RecordLogDataChangedAdapterListener.java
new file mode 100644
index 000000000..5c4196795
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/RecordLogDataChangedAdapterListener.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.listener;
+
+import org.apache.shenyu.admin.mapper.OperationRecordLogMapper;
+import org.apache.shenyu.admin.model.entity.OperationRecordLog;
+import org.apache.shenyu.admin.model.event.AdminDataModelChangedEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * RecordLogDataChangedAdapterListener.
+ */
+@Component
+public class RecordLogDataChangedAdapterListener implements 
DataChangedListener, ApplicationListener<AdminDataModelChangedEvent> {
+    
+    private final OperationRecordLogMapper logMapper;
+    
+    public RecordLogDataChangedAdapterListener(final OperationRecordLogMapper 
logMapper) {
+        this.logMapper = logMapper;
+    }
+    
+    @Override
+    public void onApplicationEvent(final AdminDataModelChangedEvent event) {
+        if (event.isConsumed()) {
+            return;
+        }
+        final OperationRecordLog log = new OperationRecordLog();
+        log.setColor(event.getType().getColor());
+        log.setContext(event.buildContext());
+        log.setOperationTime(event.getDate());
+        log.setOperationType(event.getType().getTypeName());
+        log.setOperator(event.getOperator());
+        logMapper.insert(log);
+        event.consumed();
+    }
+    
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/OperationRecordLogMapper.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/OperationRecordLogMapper.java
new file mode 100644
index 000000000..26b30de38
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/OperationRecordLogMapper.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.shenyu.admin.model.entity.OperationRecordLog;
+
+import java.util.List;
+
+/**
+ * OperationRecordLogMapper.
+ */
+@Mapper
+public interface OperationRecordLogMapper {
+    
+    /**
+     * select limit.
+     *
+     * @param limit limit
+     * @return list
+     */
+    List<OperationRecordLog> selectLimit(@Param("limit") Integer limit);
+    
+    /**
+     * insert.
+     *
+     * @param recordLog log
+     * @return count change
+     */
+    int insert(OperationRecordLog recordLog);
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/OperationRecordLog.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/OperationRecordLog.java
new file mode 100644
index 000000000..c3955e97a
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/OperationRecordLog.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.util.Date;
+
+/**
+ * operation_record_log.
+ */
+public class OperationRecordLog {
+    
+    /**
+     * id.
+     */
+    private Long id;
+    
+    /**
+     * color.
+     */
+    private String color;
+    
+    /**
+     * context.
+     */
+    private String context;
+    
+    /**
+     * operator.
+     */
+    private String operator;
+    
+    /**
+     * operation time.
+     */
+    @JsonFormat(pattern = "MM-dd hh:mm:ss")
+    private Date operationTime;
+    
+    /**
+     * operation type.
+     */
+    private String operationType;
+    
+    /**
+     * get id.
+     *
+     * @return id
+     */
+    public Long getId() {
+        return id;
+    }
+    
+    /**
+     * set id.
+     *
+     * @param id id
+     */
+    public void setId(final Long id) {
+        this.id = id;
+    }
+    
+    /**
+     * get color.
+     *
+     * @return color
+     */
+    public String getColor() {
+        return color;
+    }
+    
+    /**
+     * set color.
+     *
+     * @param color color
+     */
+    public void setColor(final String color) {
+        this.color = color;
+    }
+    
+    /**
+     * get context.
+     *
+     * @return context
+     */
+    public String getContext() {
+        return context;
+    }
+    
+    /**
+     * set context.
+     *
+     * @param context context
+     */
+    public void setContext(final String context) {
+        this.context = context;
+    }
+    
+    /**
+     * get operator.
+     *
+     * @return operator
+     */
+    public String getOperator() {
+        return operator;
+    }
+    
+    /**
+     * set operator.
+     *
+     * @param operator operator
+     */
+    public void setOperator(final String operator) {
+        this.operator = operator;
+    }
+    
+    /**
+     * get operationTime.
+     *
+     * @return operationTime
+     */
+    public Date getOperationTime() {
+        return operationTime;
+    }
+    
+    /**
+     * set operationTime.
+     *
+     * @param operationTime operationTime
+     */
+    public void setOperationTime(final Date operationTime) {
+        this.operationTime = operationTime;
+    }
+    
+    /**
+     * get operationType.
+     *
+     * @return operationType
+     */
+    public String getOperationType() {
+        return operationType;
+    }
+    
+    /**
+     * set operationType.
+     *
+     * @param operationType operationType
+     */
+    public void setOperationType(final String operationType) {
+        this.operationType = operationType;
+    }
+    
+    @Override
+    public String toString() {
+        return "OperationRecordLog{"
+                + "id=" + id
+                + ", color='" + color + '\''
+                + ", context='" + context + '\''
+                + ", operator='" + operator + '\''
+                + ", operationTime=" + operationTime
+                + ", operationType='" + operationType + '\''
+                + '}';
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/enums/EventTypeEnum.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/enums/EventTypeEnum.java
new file mode 100644
index 000000000..58771af5c
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/enums/EventTypeEnum.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.enums;
+
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+
+/**
+ * EventTypeEnum.
+ */
+public enum EventTypeEnum {
+    
+    // ============== created ===================
+    /**
+     * created event.
+     */
+    CREATE(DataEventTypeEnum.CREATE, "green"),
+    
+    /**
+     * register event.
+     */
+    REGISTER("REGISTER", DataEventTypeEnum.CREATE, "#1f640a"),
+    
+    /**
+     * plugin created event.
+     */
+    PLUGIN_CREATE("CREATE:PLUGIN", DataEventTypeEnum.CREATE, "green"),
+    
+    // ============== delete ===================
+    /**
+     * deleted event.
+     */
+    DELETE(DataEventTypeEnum.DELETE, "red"),
+    
+    /**
+     * clean event.
+     */
+    CLEAN(DataEventTypeEnum.DELETE, "#e42c64"),
+    
+    /**
+     * plugin deleted event.
+     */
+    PLUGIN_DELETE("DELETE:PLUGIN", DataEventTypeEnum.DELETE, "red"),
+    
+    // ============== update ===================
+    
+    /**
+     * update event.
+     */
+    UPDATE(DataEventTypeEnum.UPDATE, "yellow"),
+    
+    /**
+     * plugin update.
+     */
+    PLUGIN_UPDATE("UPDATE:PLUGIN", DataEventTypeEnum.UPDATE, "yellow");
+    
+    /**
+     * type name.
+     */
+    private final String typeName;
+    
+    /**
+     * type.
+     */
+    private final DataEventTypeEnum type;
+    
+    /**
+     * color.
+     */
+    private final String color;
+    
+    EventTypeEnum(final DataEventTypeEnum type, final String color) {
+        this(type.toString(), type, color);
+    }
+    
+    EventTypeEnum(final String typeName, final DataEventTypeEnum type, final 
String color) {
+        this.typeName = typeName;
+        this.type = type;
+        this.color = color;
+    }
+    
+    /**
+     * get typeName.
+     *
+     * @return type
+     */
+    public String getTypeName() {
+        return typeName;
+    }
+    
+    /**
+     * get type.
+     *
+     * @return DataEventTypeEnum
+     */
+    public DataEventTypeEnum getType() {
+        return type;
+    }
+    
+    
+    /**
+     * get color.
+     *
+     * @return color
+     */
+    public String getColor() {
+        return color;
+    }
+    
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/AdminDataModelChangedEvent.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/AdminDataModelChangedEvent.java
new file mode 100644
index 000000000..f0286acd2
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/AdminDataModelChangedEvent.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.event;
+
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+import org.springframework.context.ApplicationEvent;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * AdminDataModelChangedEvent.
+ */
+public class AdminDataModelChangedEvent extends ApplicationEvent {
+    
+    /**
+     * action type.
+     */
+    private final EventTypeEnum type;
+    
+    /**
+     * before data.
+     */
+    private final Object before;
+    
+    /**
+     * after data.
+     */
+    private final Object after;
+    
+    /**
+     * operator:is user or app.
+     */
+    private final String operator;
+    
+    /**
+     * consumed.
+     */
+    private boolean consumed;
+    
+    /**
+     * event date.
+     */
+    private final Date date;
+    
+    /**
+     * Create a new {@code ApplicationEvent}.operator is unknown.
+     *
+     * @param source Current data state
+     * @param before Before the change data state
+     * @param type   event type
+     */
+    public AdminDataModelChangedEvent(final Object source, final Object 
before, final EventTypeEnum type) {
+        this(source, before, type, null);
+    }
+    
+    /**
+     * Create a new {@code ApplicationEvent}.
+     *
+     * @param source   Current data state
+     * @param before   Before the change data state
+     * @param type     event type
+     * @param operator operator,default is unknown
+     */
+    public AdminDataModelChangedEvent(final Object source, final Object 
before, final EventTypeEnum type, final String operator) {
+        super(source);
+        this.type = type;
+        this.before = before;
+        this.after = source;
+        this.operator = Objects.isNull(operator) ? "unknown" : operator;
+        this.consumed = false;
+        this.date = new Date();
+    }
+    
+    
+    /**
+     * get type.
+     *
+     * @return type
+     */
+    public EventTypeEnum getType() {
+        return type;
+    }
+    
+    /**
+     * get before.
+     *
+     * @return before data
+     */
+    public Object getBefore() {
+        return before;
+    }
+    
+    /**
+     * get after.
+     *
+     * @return after data
+     */
+    public Object getAfter() {
+        return after;
+    }
+    
+    /**
+     * get operator.
+     *
+     * @return operator
+     */
+    public String getOperator() {
+        return operator;
+    }
+    
+    /**
+     * consumed.
+     */
+    public void consumed() {
+        this.consumed = true;
+    }
+    
+    /**
+     * is consumed.
+     *
+     * @return is consumed
+     */
+    public boolean isConsumed() {
+        return consumed;
+    }
+    
+    
+    /**
+     * get date.
+     *
+     * @return event date
+     */
+    public Date getDate() {
+        return date;
+    }
+    
+    /**
+     * before data snapshot.
+     *
+     * @return snapshot
+     */
+    public String beforeSnapshot() {
+        return Objects.toString(before, "before unknown");
+    }
+    
+    /**
+     * after data snapshot.
+     *
+     * @return snapshot
+     */
+    public String afterSnapshot() {
+        return Objects.toString(after, "after unknown");
+    }
+    
+    /**
+     * build event context.
+     *
+     * @return event context
+     */
+    public String buildContext() {
+        return String.format("%s changed(%s)[%s = > %s]", eventName(), 
type.getTypeName(), beforeSnapshot(), afterSnapshot());
+    }
+    
+    /**
+     * event name.
+     *
+     * @return name
+     */
+    public String eventName() {
+        return "data";
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/BatchChangedEvent.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/BatchChangedEvent.java
new file mode 100644
index 000000000..41eebd34e
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/BatchChangedEvent.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.event;
+
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * BatchChangedEvent.
+ */
+public class BatchChangedEvent extends AdminDataModelChangedEvent {
+    
+    
+    /**
+     * Create a new {@code PluginChangedEvent}.operator is unknown.
+     *
+     * @param source Current plugin state
+     * @param before Before the change plugin state
+     * @param type   event type
+     */
+    public BatchChangedEvent(final Collection<?> source, final Collection<?> 
before, final EventTypeEnum type, final String operator) {
+        super(source, before, type, operator);
+    }
+    
+    /**
+     * before plguin snapshot.
+     *
+     * @return snapshot
+     */
+    @Override
+    public String beforeSnapshot() {
+        // format plugin data
+        return Objects.toString(getBefore(), "before plugin unknown");
+    }
+    
+    /**
+     * after plugin snapshot.
+     *
+     * @return snapshot
+     */
+    @Override
+    public String afterSnapshot() {
+        // format plugin data
+        return Objects.toString(getAfter(), "after plugin unknown");
+    }
+    
+    @Override
+    public String eventName() {
+        return "plugin";
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/BatchPluginChangedEvent.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/BatchPluginChangedEvent.java
new file mode 100644
index 000000000..da552eb76
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/BatchPluginChangedEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.event;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+/**
+ * BatchPluginChangedEvent.
+ */
+public class BatchPluginChangedEvent extends BatchChangedEvent {
+    
+    
+    /**
+     * Create a new {@code PluginChangedEvent}.operator is unknown.
+     *
+     * @param source Current plugin state
+     * @param before Before the change plugin state
+     * @param type   event type
+     */
+    public BatchPluginChangedEvent(final Collection<PluginDO> source, final 
Collection<PluginDO> before, final EventTypeEnum type, final String operator) {
+        super(source, before, type, operator);
+    }
+    
+    @Override
+    public String buildContext() {
+        final String plugins = ((Collection<?>) getSource())
+                .stream()
+                .map(s -> ((PluginDO) s).getName())
+                .collect(Collectors.joining(","));
+        return String.format("the plugins[%s] is %s", plugins, 
StringUtils.lowerCase(getType().getType().toString()));
+    }
+    
+    @Override
+    public String eventName() {
+        return "plugin";
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/PluginChangedEvent.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/PluginChangedEvent.java
new file mode 100644
index 000000000..9bacf6a06
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/event/PluginChangedEvent.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.event;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+
+import java.util.Objects;
+
+/**
+ * AdminDataModelChangedEvent.
+ */
+public class PluginChangedEvent extends AdminDataModelChangedEvent {
+    
+    
+    /**
+     * Create a new {@code PluginChangedEvent}.operator is unknown.
+     *
+     * @param source Current plugin state
+     * @param before Before the change plugiin state
+     * @param type   event type
+     */
+    public PluginChangedEvent(final PluginDO source, final PluginDO before, 
final EventTypeEnum type, final String operator) {
+        super(source, before, type, operator);
+    }
+    
+    @Override
+    public String buildContext() {
+        final PluginDO after = (PluginDO) getAfter();
+        if (Objects.isNull(getBefore())) {
+            return String.format("the plugin [%s] is %s", after.getName(), 
StringUtils.lowerCase(getType().getType().toString()));
+        }
+        return String.format("the plugin [%s] is %s : %s", after.getName(), 
StringUtils.lowerCase(getType().getType().toString()), contrast());
+        
+    }
+    
+    private String contrast() {
+        final PluginDO before = (PluginDO) getBefore();
+        Objects.requireNonNull(before);
+        final PluginDO after = (PluginDO) getAfter();
+        Objects.requireNonNull(after);
+        if (Objects.equals(before, after)) {
+            return "it no change";
+        }
+        final StringBuilder builder = new StringBuilder();
+        if (!Objects.equals(before.getName(), after.getName())) {
+            builder.append(String.format("name[%s => %s] ", before.getName(), 
after.getName()));
+        }
+        if (!Objects.equals(before.getConfig(), after.getConfig())) {
+            builder.append(String.format("config[%s => %s] ", 
before.getConfig(), after.getConfig()));
+        }
+        if (!Objects.equals(before.getRole(), after.getRole())) {
+            builder.append(String.format("role[%s => %s] ", before.getRole(), 
after.getRole()));
+        }
+        if (!Objects.equals(before.getEnabled(), after.getEnabled())) {
+            builder.append(String.format("enable[%s => %s] ", 
before.getEnabled(), after.getEnabled()));
+        }
+        if (!Objects.equals(before.getSort(), after.getSort())) {
+            builder.append(String.format("sort[%s => %s] ", before.getSort(), 
after.getSort()));
+        }
+        return builder.toString();
+    }
+    
+    @Override
+    public String eventName() {
+        return "plugin";
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/OperationRecordLogService.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/OperationRecordLogService.java
new file mode 100644
index 000000000..e4bb319a5
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/OperationRecordLogService.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service;
+
+import org.apache.shenyu.admin.model.entity.OperationRecordLog;
+
+import java.util.List;
+
+/**
+ * OperationRecordLogService.
+ */
+public interface OperationRecordLogService {
+    
+    /**
+     * list.
+     *
+     * @return list
+     */
+    List<OperationRecordLog> list();
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/OperationRecordLogServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/OperationRecordLogServiceImpl.java
new file mode 100644
index 000000000..854f32e79
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/OperationRecordLogServiceImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.impl;
+
+import org.apache.shenyu.admin.config.properties.DashboardProperties;
+import org.apache.shenyu.admin.mapper.OperationRecordLogMapper;
+import org.apache.shenyu.admin.model.entity.OperationRecordLog;
+import org.apache.shenyu.admin.service.OperationRecordLogService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * OperationRecordLogServiceImpl.
+ */
+@Service
+public class OperationRecordLogServiceImpl implements 
OperationRecordLogService {
+    
+    private final OperationRecordLogMapper recordLogMapper;
+    
+    private final DashboardProperties dashboardProperties;
+    
+    public OperationRecordLogServiceImpl(final OperationRecordLogMapper 
recordLogMapper,
+                                         final DashboardProperties 
dashboardProperties) {
+        this.recordLogMapper = recordLogMapper;
+        this.dashboardProperties = dashboardProperties;
+    }
+    
+    @Override
+    public List<OperationRecordLog> list() {
+        return 
recordLogMapper.selectLimit(dashboardProperties.getRecordLogLimit());
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
index aa7b3d16a..94385a694 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
@@ -20,7 +20,6 @@ package org.apache.shenyu.admin.service.impl;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.shenyu.admin.aspect.annotation.Pageable;
-import org.apache.shenyu.admin.listener.DataChangedEvent;
 import org.apache.shenyu.admin.mapper.PluginHandleMapper;
 import org.apache.shenyu.admin.mapper.PluginMapper;
 import org.apache.shenyu.admin.mapper.RuleConditionMapper;
@@ -42,6 +41,7 @@ import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.model.vo.ResourceVO;
 import org.apache.shenyu.admin.service.PluginService;
 import org.apache.shenyu.admin.service.ResourceService;
+import org.apache.shenyu.admin.service.publish.PluginEventPublisher;
 import org.apache.shenyu.admin.transfer.PluginTransfer;
 import org.apache.shenyu.admin.utils.Assert;
 import org.apache.shenyu.admin.utils.ShenyuResultMessage;
@@ -50,8 +50,6 @@ import org.apache.shenyu.common.dto.PluginData;
 import org.apache.shenyu.common.enums.AdminPluginOperateEnum;
 import org.apache.shenyu.common.enums.AdminResourceEnum;
 import org.apache.shenyu.common.enums.ConfigGroupEnum;
-import org.apache.shenyu.common.enums.DataEventTypeEnum;
-import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -79,26 +77,26 @@ public class PluginServiceImpl implements PluginService {
     
     private final RuleConditionMapper ruleConditionMapper;
     
-    private final ApplicationEventPublisher eventPublisher;
-    
     private final ResourceService resourceService;
     
+    private final PluginEventPublisher modelDataEventPublisher;
+    
     public PluginServiceImpl(final PluginMapper pluginMapper,
                              final PluginHandleMapper pluginHandleMapper,
                              final SelectorMapper selectorMapper,
                              final SelectorConditionMapper 
selectorConditionMapper,
                              final RuleMapper ruleMapper,
                              final RuleConditionMapper ruleConditionMapper,
-                             final ApplicationEventPublisher eventPublisher,
-                             final ResourceService resourceService) {
+                             final ResourceService resourceService,
+                             final PluginEventPublisher 
modelDataEventPublisher) {
         this.pluginMapper = pluginMapper;
         this.pluginHandleMapper = pluginHandleMapper;
         this.selectorMapper = selectorMapper;
         this.selectorConditionMapper = selectorConditionMapper;
         this.ruleMapper = ruleMapper;
         this.ruleConditionMapper = ruleConditionMapper;
-        this.eventPublisher = eventPublisher;
         this.resourceService = resourceService;
+        this.modelDataEventPublisher = modelDataEventPublisher;
     }
     
     @Override
@@ -134,9 +132,11 @@ public class PluginServiceImpl implements PluginService {
         }
         final List<String> pluginIds = plugins.stream()
                 .map(PluginDO::getId).collect(Collectors.toList());
-
+        
         // 2. delete plugins.
         this.pluginMapper.deleteByIds(pluginIds);
+        
+        // TODO move to plugin handle service, listen plugin delete event 
execute
         // 3. delete plugin handle.
         this.pluginHandleMapper.deleteByPluginIds(pluginIds);
         
@@ -170,8 +170,7 @@ public class PluginServiceImpl implements PluginService {
         }
         
         // 6. publish change event.
-        eventPublisher.publishEvent(new 
DataChangedEvent(ConfigGroupEnum.PLUGIN, DataEventTypeEnum.DELETE,
-                
plugins.stream().map(PluginTransfer.INSTANCE::mapToData).collect(Collectors.toList())));
+        modelDataEventPublisher.onDeleted(plugins);
         return StringUtils.EMPTY;
     }
     
@@ -192,8 +191,7 @@ public class PluginServiceImpl implements PluginService {
         pluginMapper.updateEnableByIdList(ids, enabled);
         // publish change event.
         if (CollectionUtils.isNotEmpty(plugins)) {
-            eventPublisher.publishEvent(new 
DataChangedEvent(ConfigGroupEnum.PLUGIN, DataEventTypeEnum.UPDATE,
-                    
plugins.stream().map(PluginTransfer.INSTANCE::mapToData).collect(Collectors.toList())));
+            modelDataEventPublisher.onEnabled(plugins);
         }
         return StringUtils.EMPTY;
     }
@@ -335,7 +333,7 @@ public class PluginServiceImpl implements PluginService {
         
         insertPluginMenuResource(resourceDO);
     }
-
+    
     /**
      * create plugin.<br>
      * insert plugin and insert plugin data.
@@ -348,13 +346,11 @@ public class PluginServiceImpl implements PluginService {
         PluginDO pluginDO = PluginDO.buildPluginDO(pluginDTO);
         insertPluginDataToResource(pluginDTO);
         pluginMapper.insertSelective(pluginDO);
-
         // publish create event.
-        eventPublisher.publishEvent(new 
DataChangedEvent(ConfigGroupEnum.PLUGIN, DataEventTypeEnum.CREATE,
-                
Collections.singletonList(PluginTransfer.INSTANCE.mapToData(pluginDO))));
+        modelDataEventPublisher.onCreated(pluginDO);
         return ShenyuResultMessage.CREATE_SUCCESS;
     }
-
+    
     /**
      * update plugin.<br>
      *
@@ -363,12 +359,12 @@ public class PluginServiceImpl implements PluginService {
      */
     private String update(final PluginDTO pluginDTO) {
         Assert.isNull(pluginMapper.nameExistedExclude(pluginDTO.getName(), 
Collections.singletonList(pluginDTO.getId())), 
AdminConstants.PLUGIN_NAME_IS_EXIST);
+        final PluginDO before = pluginMapper.selectById(pluginDTO.getId());
         PluginDO pluginDO = PluginDO.buildPluginDO(pluginDTO);
         pluginMapper.updateSelective(pluginDO);
-
+        
         // publish update event.
-        eventPublisher.publishEvent(new 
DataChangedEvent(ConfigGroupEnum.PLUGIN, DataEventTypeEnum.UPDATE,
-                
Collections.singletonList(PluginTransfer.INSTANCE.mapToData(pluginDO))));
+        modelDataEventPublisher.onUpdated(pluginDO, before);
         return ShenyuResultMessage.UPDATE_SUCCESS;
     }
 }
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/publish/AdminDataModelChangedEventPublisher.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/publish/AdminDataModelChangedEventPublisher.java
new file mode 100644
index 000000000..b6dc616bf
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/publish/AdminDataModelChangedEventPublisher.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.publish;
+
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+import org.apache.shenyu.admin.model.event.AdminDataModelChangedEvent;
+import org.apache.shenyu.admin.model.event.BatchChangedEvent;
+import org.apache.shenyu.admin.utils.SessionUtil;
+
+import java.util.Collection;
+
+/**
+ * ModelDataEventPublisher.
+ */
+public interface AdminDataModelChangedEventPublisher<T> {
+    
+    /**
+     * on  created.
+     *
+     * @param data data
+     */
+    default void onCreated(final T data) {
+        publish(new AdminDataModelChangedEvent(data, null, 
EventTypeEnum.CREATE, SessionUtil.visitorName()));
+    }
+    
+    /**
+     * on  created.
+     *
+     * @param data data
+     */
+    default void onCreated(final Collection<T> data) {
+        publish(new BatchChangedEvent(data, null, EventTypeEnum.CREATE, 
SessionUtil.visitorName()));
+    }
+    
+    /**
+     * on data updated.
+     *
+     * @param data   data
+     * @param before before data
+     */
+    default void onUpdated(final T data, final T before) {
+        publish(new AdminDataModelChangedEvent(data, before, 
EventTypeEnum.UPDATE, SessionUtil.visitorName()));
+    }
+    
+    /**
+     * on data updated.
+     *
+     * @param data   data
+     * @param before before data
+     */
+    default void onUpdated(final Collection<T> data, final Collection<T> 
before) {
+        publish(new BatchChangedEvent(data, before, EventTypeEnum.UPDATE, 
SessionUtil.visitorName()));
+    }
+    
+    /**
+     * on data deleted.
+     *
+     * @param data data
+     */
+    default void onDeleted(final T data) {
+        publish(new AdminDataModelChangedEvent(data, null, 
EventTypeEnum.DELETE, SessionUtil.visitorName()));
+    }
+    
+    
+    /**
+     * on data deleted.
+     *
+     * @param data data
+     */
+    default void onDeleted(final Collection<T> data) {
+        publish(new BatchChangedEvent(data, null, EventTypeEnum.DELETE, 
SessionUtil.visitorName()));
+    }
+    
+    /**
+     * event.
+     *
+     * @param event event.
+     */
+    void publish(AdminDataModelChangedEvent event);
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/publish/PluginEventPublisher.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/publish/PluginEventPublisher.java
new file mode 100644
index 000000000..b029e9afc
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/publish/PluginEventPublisher.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.publish;
+
+import org.apache.shenyu.admin.listener.DataChangedEvent;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+import org.apache.shenyu.admin.model.event.AdminDataModelChangedEvent;
+import org.apache.shenyu.admin.model.event.BatchPluginChangedEvent;
+import org.apache.shenyu.admin.model.event.PluginChangedEvent;
+import org.apache.shenyu.admin.transfer.PluginTransfer;
+import org.apache.shenyu.admin.utils.SessionUtil;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * ModelDataEventPublisher.
+ */
+@Component
+public class PluginEventPublisher implements 
AdminDataModelChangedEventPublisher<PluginDO> {
+    
+    private final ApplicationEventPublisher publisher;
+    
+    public PluginEventPublisher(final ApplicationEventPublisher publisher) {
+        this.publisher = publisher;
+    }
+    
+    /**
+     * on plugin created.
+     *
+     * @param plugin plugin
+     */
+    @Override
+    public void onCreated(final PluginDO plugin) {
+        publisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, 
DataEventTypeEnum.CREATE,
+                
Collections.singletonList(PluginTransfer.INSTANCE.mapToData(plugin))));
+        publish(new PluginChangedEvent(plugin, null, 
EventTypeEnum.PLUGIN_CREATE, SessionUtil.visitorName()));
+    }
+    
+    /**
+     * on plugin updated.
+     *
+     * @param plugin plugin
+     * @param before before plugin
+     */
+    @Override
+    public void onUpdated(final PluginDO plugin, final PluginDO before) {
+        publisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, 
DataEventTypeEnum.UPDATE,
+                
Collections.singletonList(PluginTransfer.INSTANCE.mapToData(plugin))));
+        publish(new PluginChangedEvent(plugin, before, 
EventTypeEnum.PLUGIN_UPDATE, SessionUtil.visitorName()));
+    }
+    
+    /**
+     * on plugin deleted.
+     *
+     * @param plugin plugin
+     */
+    @Override
+    public void onDeleted(final PluginDO plugin) {
+        publish(new PluginChangedEvent(plugin, null, 
EventTypeEnum.PLUGIN_DELETE, SessionUtil.visitorName()));
+        publisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, 
DataEventTypeEnum.DELETE,
+                
Stream.of(plugin).map(PluginTransfer.INSTANCE::mapToData).collect(Collectors.toList())));
+    }
+    
+    /**
+     * on plugin deleted.
+     *
+     * @param plugins plugins
+     */
+    @Override
+    public void onDeleted(final Collection<PluginDO> plugins) {
+        publish(new BatchPluginChangedEvent(plugins, null, 
EventTypeEnum.PLUGIN_DELETE, SessionUtil.visitorName()));
+        publisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, 
DataEventTypeEnum.DELETE,
+                
plugins.stream().map(PluginTransfer.INSTANCE::mapToData).collect(Collectors.toList())));
+    }
+    
+    /**
+     * on plugin batch enabled.
+     *
+     * @param plugins plugins
+     */
+    public void onEnabled(final Collection<PluginDO> plugins) {
+        publish(new BatchPluginChangedEvent(plugins, null, 
EventTypeEnum.PLUGIN_UPDATE, SessionUtil.visitorName()));
+        publisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, 
DataEventTypeEnum.UPDATE,
+                
plugins.stream().map(PluginTransfer.INSTANCE::mapToData).collect(Collectors.toList())));
+    }
+    
+    /**
+     * event.
+     *
+     * @param event event.
+     */
+    @Override
+    public void publish(final AdminDataModelChangedEvent event) {
+        publisher.publishEvent(event);
+    }
+}
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/SessionUtil.java 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/SessionUtil.java
new file mode 100644
index 000000000..922b09e1c
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/SessionUtil.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.utils;
+
+import org.apache.shenyu.admin.model.custom.UserInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+
+/**
+ * the session is a request content.
+ * Suitable for retrieving user context information from a variety of sources。
+ */
+public final class SessionUtil {
+    
+    private static final Logger LOG = 
LoggerFactory.getLogger(SessionUtil.class);
+    
+    private static final ThreadLocal<UserInfo> LOCAL_VISITOR = new 
InheritableThreadLocal<>();
+    
+    private SessionUtil() {
+    }
+    
+    
+    /**
+     * visitor is login user[admin or other] / app /bootstrap.
+     *
+     * @return default is unknown
+     */
+    public static String visitorName() {
+        return visitor().getUserName();
+    }
+    
+    /**
+     * visitor is login user[admin or other] / app /bootstrap.
+     *
+     * @return default is unknown
+     */
+    public static UserInfo visitor() {
+        try {
+            final UserInfo userInfo = LOCAL_VISITOR.get();
+            if (Objects.isNull(userInfo)) {
+                // try get from auth
+                setLocalVisitorFromAuth();
+            }
+            return LOCAL_VISITOR.get();
+        } catch (Exception e) {
+            LOG.warn("get user info error ,not found, used default user ,it 
unknown");
+        }
+        return UserInfo.builder().userId("-1").userName("unknown").build();
+    }
+    
+    /**
+     * set visitor user.
+     *
+     * @param userInfo user info
+     */
+    public static void setLocalVisitor(final UserInfo userInfo) {
+        LOCAL_VISITOR.set(userInfo);
+    }
+    
+    /**
+     * set visitor user.
+     */
+    public static void setLocalVisitorFromAuth() {
+        // featureToDo:Adapting app access
+        LOCAL_VISITOR.set(JwtUtils.getUserInfo());
+    }
+    
+    /**
+     * clean current session.
+     */
+    public static void clean() {
+        LOCAL_VISITOR.remove();
+    }
+}
+
diff --git 
a/shenyu-admin/src/main/resources/mappers/operation-record-log-sqlmap.xml 
b/shenyu-admin/src/main/resources/mappers/operation-record-log-sqlmap.xml
new file mode 100644
index 000000000..a171e9352
--- /dev/null
+++ b/shenyu-admin/src/main/resources/mappers/operation-record-log-sqlmap.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"https://mybatis.org/dtd/mybatis-3-mapper.dtd";>
+<mapper namespace="org.apache.shenyu.admin.mapper.OperationRecordLogMapper">
+    <resultMap id="BaseResultMap" 
type="org.apache.shenyu.admin.model.entity.OperationRecordLog">
+        <id column="id" jdbcType="VARCHAR" property="id"/>
+        <result column="id" jdbcType="BIGINT" property="id"/>
+        <result column="color" jdbcType="VARCHAR" property="color"/>
+        <result column="context" jdbcType="VARCHAR" property="context"/>
+        <result column="operator" jdbcType="VARCHAR" property="operator"/>
+        <result column="operation_time" jdbcType="VARCHAR" 
property="operationTime"/>
+        <result column="operation_type" jdbcType="VARCHAR" 
property="operationType"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,
+        color,
+        context,
+        operator,
+        operation_time,
+        operation_type
+    </sql>
+
+    <select id="selectLimit" parameterType="java.lang.Integer" 
resultMap="BaseResultMap">
+        SELECT
+               <include refid="Base_Column_List"/>
+          FROM operation_record_log
+         order by operation_time desc
+    </select>
+
+    <insert id="insert" 
parameterType="org.apache.shenyu.admin.model.entity.OperationRecordLog"  
useGeneratedKeys="true" keyProperty="id">
+        insert into operation_record_log(id, color, context, operator, 
operation_time, operation_type)
+        values (#{id},
+                #{color},
+                #{context},
+                #{operator},
+                #{operationTime},
+                #{operationType}
+                )
+    </insert>
+</mapper>
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql 
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index c12aa48fe..51794b245 100644
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -242,6 +242,18 @@ CREATE TABLE IF NOT EXISTS `data_permission` (
     `date_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP COMMENT 'update time',
     PRIMARY KEY (`id`)
     );
+-- ----------------------------
+-- Table structure for operation_record_log
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `operation_record_log`
+(
+    `id`             bigint       NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 
'id',
+    `color`          varchar(20)  NOT NULL COMMENT 'log color',
+    `context`        text         NOT NULL COMMENT 'log context',
+    `operator`       varchar(200) NOT NULL COMMENT 'operator [user or app]]',
+    `operation_time` datetime     NOT NULL DEFAULT now() COMMENT 'operation 
time',
+    `operation_type` varchar(60)  NOT NULL DEFAULT 'update' COMMENT 'operation 
type:create/update/delete/register...'
+);
 
 /**default admin user**/
 INSERT IGNORE INTO `dashboard_user` (`id`, `user_name`, `password`, `role`, 
`enabled`) VALUES 
('1','admin','ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
 '1', '1');
diff --git a/shenyu-admin/src/main/resources/sql-script/mysql/schema.sql 
b/shenyu-admin/src/main/resources/sql-script/mysql/schema.sql
index f00704143..13687aa68 100644
--- a/shenyu-admin/src/main/resources/sql-script/mysql/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/mysql/schema.sql
@@ -248,6 +248,18 @@ CREATE TABLE IF NOT EXISTS `data_permission` (
     `date_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP COMMENT 'update time',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 
ROW_FORMAT=DYNAMIC COMMENT='data permission table';
+-- ----------------------------
+-- Table structure for operation_record_log
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `operation_record_log`
+(
+    `id`             bigint auto_increment        not null comment 'id' 
primary key,
+    `color`          varchar(20)                  not null comment 'log color',
+    `context`        text                         not null comment 'log 
context',
+    `operator`       varchar(200)                 not null comment 'operator 
[user or app]]',
+    `operation_time` datetime    default now()    not null comment 'operation 
time',
+    `operation_type` varchar(60) default 'update' not null comment 'operation 
type:create/update/delete/register...'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 
ROW_FORMAT=DYNAMIC  comment 'operation record log';
 
 /**default admin user**/
 INSERT IGNORE INTO `dashboard_user` (`id`, `user_name`, `password`, `role`, 
`enabled`) VALUES 
('1','admin','ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
 '1', '1');
diff --git a/shenyu-admin/src/main/resources/sql-script/pg/schema.sql 
b/shenyu-admin/src/main/resources/sql-script/pg/schema.sql
index 55b721179..5924953a7 100644
--- a/shenyu-admin/src/main/resources/sql-script/pg/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/pg/schema.sql
@@ -1092,6 +1092,35 @@ ELSE
 
        PERFORM public.dblink_exec('init_conn', 'COMMIT');
 END IF;
+
+-- ----------------------------------------
+-- create table operation_record_log if not exist ---
+-- ---------------------------------------
+IF (SELECT * FROM dblink('host=localhost user=' || _user || ' password=' || 
_password || ' dbname=' ||_db,'SELECT COUNT(1) FROM pg_class  WHERE relname  = 
''' ||'operation_record_log' || '''')AS t(count BIGINT) )> 0 THEN
+    RAISE NOTICE 'operation_record_log already exists';
+ELSE
+    PERFORM public.dblink_exec('init_conn', 'BEGIN');
+    PERFORM public.dblink_exec('init_conn', 'CREATE TABLE 
"operation_record_log" (
+      "id" int8 NOT NULL,
+      "color" varchar(20) COLLATE "pg_catalog"."default" NOT NULL,
+      "context" text COLLATE "pg_catalog"."default" NOT NULL,
+      "operator" varchar(200) COLLATE "pg_catalog"."default" NOT NULL,
+      "operation_time" timestamp(6) NOT NULL,
+      "operation_type" varchar(60) COLLATE "pg_catalog"."default" NOT NULL,
+      CONSTRAINT "operation_record_log_pkey" PRIMARY KEY ("id")
+    )');
+
+    PERFORM public.dblink_exec('init_conn','COMMENT ON COLUMN 
"operation_record_log"."id" IS ''' || 'id' || '''');
+    PERFORM public.dblink_exec('init_conn','COMMENT ON COLUMN 
"operation_record_log"."color" IS ''' || 'log color' || '''');
+    PERFORM public.dblink_exec('init_conn','COMMENT ON COLUMN 
"operation_record_log"."context" IS ''' || 'log context' || '''');
+    PERFORM public.dblink_exec('init_conn','COMMENT ON COLUMN 
"operation_record_log"."operator" IS ''' || 'operator [user or app]]' || '''');
+    PERFORM public.dblink_exec('init_conn','COMMENT ON COLUMN 
"operation_record_log"."operation_time" IS ''' || 'operation time' || '''');
+    PERFORM public.dblink_exec('init_conn','COMMENT ON COLUMN 
"operation_record_log"."operation_type" IS ''' || 'operation 
type:create/update/delete/register...' || '''');
+    PERFORM public.dblink_exec('init_conn','COMMENT ON TABLE 
"operation_record_log" IS ''' || 'operation record log' || '''');
+
+    PERFORM public.dblink_exec('init_conn', 'COMMIT');
+END IF;
+
        PERFORM public.dblink_disconnect('init_conn');
 END
 $do$;
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
index 2b527e0d9..167dd58f4 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PluginServiceTest.java
@@ -33,6 +33,7 @@ import org.apache.shenyu.admin.model.page.PageParameter;
 import org.apache.shenyu.admin.model.query.PluginQuery;
 import org.apache.shenyu.admin.model.vo.PluginVO;
 import org.apache.shenyu.admin.service.impl.PluginServiceImpl;
+import org.apache.shenyu.admin.service.publish.PluginEventPublisher;
 import org.apache.shenyu.admin.utils.ShenyuResultMessage;
 import org.apache.shenyu.common.constant.AdminConstants;
 import org.apache.shenyu.common.dto.PluginData;
@@ -45,7 +46,6 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
-import org.springframework.context.ApplicationEventPublisher;
 
 import java.sql.Timestamp;
 import java.time.LocalDateTime;
@@ -91,15 +91,15 @@ public final class PluginServiceTest {
     private SelectorConditionMapper selectorConditionMapper;
     
     @Mock
-    private ApplicationEventPublisher eventPublisher;
+    private ResourceService resourceService;
     
     @Mock
-    private ResourceService resourceService;
+    private PluginEventPublisher modelDataEventPublisher;
     
     @BeforeEach
     public void setUp() {
         pluginService = new PluginServiceImpl(pluginMapper, 
pluginHandleMapper, selectorMapper, selectorConditionMapper,
-                ruleMapper, ruleConditionMapper, eventPublisher, 
resourceService);
+                ruleMapper, ruleConditionMapper, resourceService, 
modelDataEventPublisher);
     }
     
     @Test

Reply via email to