diff --git a/backend/src/main/java/io/metersphere/base/domain/Notification.java b/backend/src/main/java/io/metersphere/base/domain/Notification.java
new file mode 100644
index 0000000000000000000000000000000000000000..9fb7981ea1af97daf8f42790c78987d251e92a62
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/base/domain/Notification.java
@@ -0,0 +1,23 @@
+package io.metersphere.base.domain;
+
+import java.io.Serializable;
+import lombok.Data;
+
+@Data
+public class Notification implements Serializable {
+    private Long id;
+
+    private String type;
+
+    private String receiver;
+
+    private String title;
+
+    private String status;
+
+    private Long createTime;
+
+    private String content;
+
+    private static final long serialVersionUID = 1L;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/io/metersphere/base/domain/NotificationExample.java b/backend/src/main/java/io/metersphere/base/domain/NotificationExample.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f1dbeedbded06a110a81901bda21402d25eed5b
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/base/domain/NotificationExample.java
@@ -0,0 +1,600 @@
+package io.metersphere.base.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NotificationExample {
+    protected String orderByClause;
+
+    protected boolean distinct;
+
+    protected List<Criteria> oredCriteria;
+
+    public NotificationExample() {
+        oredCriteria = new ArrayList<Criteria>();
+    }
+
+    public void setOrderByClause(String orderByClause) {
+        this.orderByClause = orderByClause;
+    }
+
+    public String getOrderByClause() {
+        return orderByClause;
+    }
+
+    public void setDistinct(boolean distinct) {
+        this.distinct = distinct;
+    }
+
+    public boolean isDistinct() {
+        return distinct;
+    }
+
+    public List<Criteria> getOredCriteria() {
+        return oredCriteria;
+    }
+
+    public void or(Criteria criteria) {
+        oredCriteria.add(criteria);
+    }
+
+    public Criteria or() {
+        Criteria criteria = createCriteriaInternal();
+        oredCriteria.add(criteria);
+        return criteria;
+    }
+
+    public Criteria createCriteria() {
+        Criteria criteria = createCriteriaInternal();
+        if (oredCriteria.size() == 0) {
+            oredCriteria.add(criteria);
+        }
+        return criteria;
+    }
+
+    protected Criteria createCriteriaInternal() {
+        Criteria criteria = new Criteria();
+        return criteria;
+    }
+
+    public void clear() {
+        oredCriteria.clear();
+        orderByClause = null;
+        distinct = false;
+    }
+
+    protected abstract static class GeneratedCriteria {
+        protected List<Criterion> criteria;
+
+        protected GeneratedCriteria() {
+            super();
+            criteria = new ArrayList<Criterion>();
+        }
+
+        public boolean isValid() {
+            return criteria.size() > 0;
+        }
+
+        public List<Criterion> getAllCriteria() {
+            return criteria;
+        }
+
+        public List<Criterion> getCriteria() {
+            return criteria;
+        }
+
+        protected void addCriterion(String condition) {
+            if (condition == null) {
+                throw new RuntimeException("Value for condition cannot be null");
+            }
+            criteria.add(new Criterion(condition));
+        }
+
+        protected void addCriterion(String condition, Object value, String property) {
+            if (value == null) {
+                throw new RuntimeException("Value for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value));
+        }
+
+        protected void addCriterion(String condition, Object value1, Object value2, String property) {
+            if (value1 == null || value2 == null) {
+                throw new RuntimeException("Between values for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value1, value2));
+        }
+
+        public Criteria andIdIsNull() {
+            addCriterion("id is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdIsNotNull() {
+            addCriterion("id is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdEqualTo(Long value) {
+            addCriterion("id =", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotEqualTo(Long value) {
+            addCriterion("id <>", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdGreaterThan(Long value) {
+            addCriterion("id >", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdGreaterThanOrEqualTo(Long value) {
+            addCriterion("id >=", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdLessThan(Long value) {
+            addCriterion("id <", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdLessThanOrEqualTo(Long value) {
+            addCriterion("id <=", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdIn(List<Long> values) {
+            addCriterion("id in", values, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotIn(List<Long> values) {
+            addCriterion("id not in", values, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdBetween(Long value1, Long value2) {
+            addCriterion("id between", value1, value2, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotBetween(Long value1, Long value2) {
+            addCriterion("id not between", value1, value2, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeIsNull() {
+            addCriterion("`type` is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeIsNotNull() {
+            addCriterion("`type` is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeEqualTo(String value) {
+            addCriterion("`type` =", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotEqualTo(String value) {
+            addCriterion("`type` <>", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeGreaterThan(String value) {
+            addCriterion("`type` >", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeGreaterThanOrEqualTo(String value) {
+            addCriterion("`type` >=", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeLessThan(String value) {
+            addCriterion("`type` <", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeLessThanOrEqualTo(String value) {
+            addCriterion("`type` <=", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeLike(String value) {
+            addCriterion("`type` like", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotLike(String value) {
+            addCriterion("`type` not like", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeIn(List<String> values) {
+            addCriterion("`type` in", values, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotIn(List<String> values) {
+            addCriterion("`type` not in", values, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeBetween(String value1, String value2) {
+            addCriterion("`type` between", value1, value2, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotBetween(String value1, String value2) {
+            addCriterion("`type` not between", value1, value2, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverIsNull() {
+            addCriterion("receiver is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverIsNotNull() {
+            addCriterion("receiver is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverEqualTo(String value) {
+            addCriterion("receiver =", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverNotEqualTo(String value) {
+            addCriterion("receiver <>", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverGreaterThan(String value) {
+            addCriterion("receiver >", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverGreaterThanOrEqualTo(String value) {
+            addCriterion("receiver >=", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverLessThan(String value) {
+            addCriterion("receiver <", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverLessThanOrEqualTo(String value) {
+            addCriterion("receiver <=", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverLike(String value) {
+            addCriterion("receiver like", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverNotLike(String value) {
+            addCriterion("receiver not like", value, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverIn(List<String> values) {
+            addCriterion("receiver in", values, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverNotIn(List<String> values) {
+            addCriterion("receiver not in", values, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverBetween(String value1, String value2) {
+            addCriterion("receiver between", value1, value2, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andReceiverNotBetween(String value1, String value2) {
+            addCriterion("receiver not between", value1, value2, "receiver");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleIsNull() {
+            addCriterion("title is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleIsNotNull() {
+            addCriterion("title is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleEqualTo(String value) {
+            addCriterion("title =", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleNotEqualTo(String value) {
+            addCriterion("title <>", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleGreaterThan(String value) {
+            addCriterion("title >", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleGreaterThanOrEqualTo(String value) {
+            addCriterion("title >=", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleLessThan(String value) {
+            addCriterion("title <", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleLessThanOrEqualTo(String value) {
+            addCriterion("title <=", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleLike(String value) {
+            addCriterion("title like", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleNotLike(String value) {
+            addCriterion("title not like", value, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleIn(List<String> values) {
+            addCriterion("title in", values, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleNotIn(List<String> values) {
+            addCriterion("title not in", values, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleBetween(String value1, String value2) {
+            addCriterion("title between", value1, value2, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andTitleNotBetween(String value1, String value2) {
+            addCriterion("title not between", value1, value2, "title");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusIsNull() {
+            addCriterion("`status` is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusIsNotNull() {
+            addCriterion("`status` is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusEqualTo(String value) {
+            addCriterion("`status` =", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotEqualTo(String value) {
+            addCriterion("`status` <>", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusGreaterThan(String value) {
+            addCriterion("`status` >", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusGreaterThanOrEqualTo(String value) {
+            addCriterion("`status` >=", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusLessThan(String value) {
+            addCriterion("`status` <", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusLessThanOrEqualTo(String value) {
+            addCriterion("`status` <=", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusLike(String value) {
+            addCriterion("`status` like", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotLike(String value) {
+            addCriterion("`status` not like", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusIn(List<String> values) {
+            addCriterion("`status` in", values, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotIn(List<String> values) {
+            addCriterion("`status` not in", values, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusBetween(String value1, String value2) {
+            addCriterion("`status` between", value1, value2, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotBetween(String value1, String value2) {
+            addCriterion("`status` not between", value1, value2, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeIsNull() {
+            addCriterion("create_time is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeIsNotNull() {
+            addCriterion("create_time is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeEqualTo(Long value) {
+            addCriterion("create_time =", value, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeNotEqualTo(Long value) {
+            addCriterion("create_time <>", value, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeGreaterThan(Long value) {
+            addCriterion("create_time >", value, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeGreaterThanOrEqualTo(Long value) {
+            addCriterion("create_time >=", value, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeLessThan(Long value) {
+            addCriterion("create_time <", value, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeLessThanOrEqualTo(Long value) {
+            addCriterion("create_time <=", value, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeIn(List<Long> values) {
+            addCriterion("create_time in", values, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeNotIn(List<Long> values) {
+            addCriterion("create_time not in", values, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeBetween(Long value1, Long value2) {
+            addCriterion("create_time between", value1, value2, "createTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeNotBetween(Long value1, Long value2) {
+            addCriterion("create_time not between", value1, value2, "createTime");
+            return (Criteria) this;
+        }
+    }
+
+    public static class Criteria extends GeneratedCriteria {
+
+        protected Criteria() {
+            super();
+        }
+    }
+
+    public static class Criterion {
+        private String condition;
+
+        private Object value;
+
+        private Object secondValue;
+
+        private boolean noValue;
+
+        private boolean singleValue;
+
+        private boolean betweenValue;
+
+        private boolean listValue;
+
+        private String typeHandler;
+
+        public String getCondition() {
+            return condition;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public Object getSecondValue() {
+            return secondValue;
+        }
+
+        public boolean isNoValue() {
+            return noValue;
+        }
+
+        public boolean isSingleValue() {
+            return singleValue;
+        }
+
+        public boolean isBetweenValue() {
+            return betweenValue;
+        }
+
+        public boolean isListValue() {
+            return listValue;
+        }
+
+        public String getTypeHandler() {
+            return typeHandler;
+        }
+
+        protected Criterion(String condition) {
+            super();
+            this.condition = condition;
+            this.typeHandler = null;
+            this.noValue = true;
+        }
+
+        protected Criterion(String condition, Object value, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.typeHandler = typeHandler;
+            if (value instanceof List<?>) {
+                this.listValue = true;
+            } else {
+                this.singleValue = true;
+            }
+        }
+
+        protected Criterion(String condition, Object value) {
+            this(condition, value, null);
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.secondValue = secondValue;
+            this.typeHandler = typeHandler;
+            this.betweenValue = true;
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue) {
+            this(condition, value, secondValue, null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/io/metersphere/base/mapper/NotificationMapper.java b/backend/src/main/java/io/metersphere/base/mapper/NotificationMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..6af1de0c1b7cf615127628f6281a774d2c0e5157
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/base/mapper/NotificationMapper.java
@@ -0,0 +1,36 @@
+package io.metersphere.base.mapper;
+
+import io.metersphere.base.domain.Notification;
+import io.metersphere.base.domain.NotificationExample;
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+
+public interface NotificationMapper {
+    long countByExample(NotificationExample example);
+
+    int deleteByExample(NotificationExample example);
+
+    int deleteByPrimaryKey(Long id);
+
+    int insert(Notification record);
+
+    int insertSelective(Notification record);
+
+    List<Notification> selectByExampleWithBLOBs(NotificationExample example);
+
+    List<Notification> selectByExample(NotificationExample example);
+
+    Notification selectByPrimaryKey(Long id);
+
+    int updateByExampleSelective(@Param("record") Notification record, @Param("example") NotificationExample example);
+
+    int updateByExampleWithBLOBs(@Param("record") Notification record, @Param("example") NotificationExample example);
+
+    int updateByExample(@Param("record") Notification record, @Param("example") NotificationExample example);
+
+    int updateByPrimaryKeySelective(Notification record);
+
+    int updateByPrimaryKeyWithBLOBs(Notification record);
+
+    int updateByPrimaryKey(Notification record);
+}
\ No newline at end of file
diff --git a/backend/src/main/java/io/metersphere/base/mapper/NotificationMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/NotificationMapper.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cc90b7695afa9898745b95281a59761522ca9d2c
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/base/mapper/NotificationMapper.xml
@@ -0,0 +1,287 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="io.metersphere.base.mapper.NotificationMapper">
+  <resultMap id="BaseResultMap" type="io.metersphere.base.domain.Notification">
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="type" jdbcType="VARCHAR" property="type" />
+    <result column="receiver" jdbcType="VARCHAR" property="receiver" />
+    <result column="title" jdbcType="VARCHAR" property="title" />
+    <result column="status" jdbcType="VARCHAR" property="status" />
+    <result column="create_time" jdbcType="BIGINT" property="createTime" />
+  </resultMap>
+  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.Notification">
+    <result column="content" jdbcType="LONGVARCHAR" property="content" />
+  </resultMap>
+  <sql id="Example_Where_Clause">
+    <where>
+      <foreach collection="oredCriteria" item="criteria" separator="or">
+        <if test="criteria.valid">
+          <trim prefix="(" prefixOverrides="and" suffix=")">
+            <foreach collection="criteria.criteria" item="criterion">
+              <choose>
+                <when test="criterion.noValue">
+                  and ${criterion.condition}
+                </when>
+                <when test="criterion.singleValue">
+                  and ${criterion.condition} #{criterion.value}
+                </when>
+                <when test="criterion.betweenValue">
+                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
+                </when>
+                <when test="criterion.listValue">
+                  and ${criterion.condition}
+                  <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
+                    #{listItem}
+                  </foreach>
+                </when>
+              </choose>
+            </foreach>
+          </trim>
+        </if>
+      </foreach>
+    </where>
+  </sql>
+  <sql id="Update_By_Example_Where_Clause">
+    <where>
+      <foreach collection="example.oredCriteria" item="criteria" separator="or">
+        <if test="criteria.valid">
+          <trim prefix="(" prefixOverrides="and" suffix=")">
+            <foreach collection="criteria.criteria" item="criterion">
+              <choose>
+                <when test="criterion.noValue">
+                  and ${criterion.condition}
+                </when>
+                <when test="criterion.singleValue">
+                  and ${criterion.condition} #{criterion.value}
+                </when>
+                <when test="criterion.betweenValue">
+                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
+                </when>
+                <when test="criterion.listValue">
+                  and ${criterion.condition}
+                  <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
+                    #{listItem}
+                  </foreach>
+                </when>
+              </choose>
+            </foreach>
+          </trim>
+        </if>
+      </foreach>
+    </where>
+  </sql>
+  <sql id="Base_Column_List">
+    id, `type`, receiver, title, `status`, create_time
+  </sql>
+  <sql id="Blob_Column_List">
+    content
+  </sql>
+  <select id="selectByExampleWithBLOBs" parameterType="io.metersphere.base.domain.NotificationExample" resultMap="ResultMapWithBLOBs">
+    select
+    <if test="distinct">
+      distinct
+    </if>
+    <include refid="Base_Column_List" />
+    ,
+    <include refid="Blob_Column_List" />
+    from notification
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+    <if test="orderByClause != null">
+      order by ${orderByClause}
+    </if>
+  </select>
+  <select id="selectByExample" parameterType="io.metersphere.base.domain.NotificationExample" resultMap="BaseResultMap">
+    select
+    <if test="distinct">
+      distinct
+    </if>
+    <include refid="Base_Column_List" />
+    from notification
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+    <if test="orderByClause != null">
+      order by ${orderByClause}
+    </if>
+  </select>
+  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="ResultMapWithBLOBs">
+    select 
+    <include refid="Base_Column_List" />
+    ,
+    <include refid="Blob_Column_List" />
+    from notification
+    where id = #{id,jdbcType=BIGINT}
+  </select>
+  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
+    delete from notification
+    where id = #{id,jdbcType=BIGINT}
+  </delete>
+  <delete id="deleteByExample" parameterType="io.metersphere.base.domain.NotificationExample">
+    delete from notification
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+  </delete>
+  <insert id="insert" parameterType="io.metersphere.base.domain.Notification">
+    insert into notification (id, `type`, receiver, 
+      title, `status`, create_time, 
+      content)
+    values (#{id,jdbcType=BIGINT}, #{type,jdbcType=VARCHAR}, #{receiver,jdbcType=VARCHAR}, 
+      #{title,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}, #{createTime,jdbcType=BIGINT}, 
+      #{content,jdbcType=LONGVARCHAR})
+  </insert>
+  <insert id="insertSelective" parameterType="io.metersphere.base.domain.Notification">
+    insert into notification
+    <trim prefix="(" suffix=")" suffixOverrides=",">
+      <if test="id != null">
+        id,
+      </if>
+      <if test="type != null">
+        `type`,
+      </if>
+      <if test="receiver != null">
+        receiver,
+      </if>
+      <if test="title != null">
+        title,
+      </if>
+      <if test="status != null">
+        `status`,
+      </if>
+      <if test="createTime != null">
+        create_time,
+      </if>
+      <if test="content != null">
+        content,
+      </if>
+    </trim>
+    <trim prefix="values (" suffix=")" suffixOverrides=",">
+      <if test="id != null">
+        #{id,jdbcType=BIGINT},
+      </if>
+      <if test="type != null">
+        #{type,jdbcType=VARCHAR},
+      </if>
+      <if test="receiver != null">
+        #{receiver,jdbcType=VARCHAR},
+      </if>
+      <if test="title != null">
+        #{title,jdbcType=VARCHAR},
+      </if>
+      <if test="status != null">
+        #{status,jdbcType=VARCHAR},
+      </if>
+      <if test="createTime != null">
+        #{createTime,jdbcType=BIGINT},
+      </if>
+      <if test="content != null">
+        #{content,jdbcType=LONGVARCHAR},
+      </if>
+    </trim>
+  </insert>
+  <select id="countByExample" parameterType="io.metersphere.base.domain.NotificationExample" resultType="java.lang.Long">
+    select count(*) from notification
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+  </select>
+  <update id="updateByExampleSelective" parameterType="map">
+    update notification
+    <set>
+      <if test="record.id != null">
+        id = #{record.id,jdbcType=BIGINT},
+      </if>
+      <if test="record.type != null">
+        `type` = #{record.type,jdbcType=VARCHAR},
+      </if>
+      <if test="record.receiver != null">
+        receiver = #{record.receiver,jdbcType=VARCHAR},
+      </if>
+      <if test="record.title != null">
+        title = #{record.title,jdbcType=VARCHAR},
+      </if>
+      <if test="record.status != null">
+        `status` = #{record.status,jdbcType=VARCHAR},
+      </if>
+      <if test="record.createTime != null">
+        create_time = #{record.createTime,jdbcType=BIGINT},
+      </if>
+      <if test="record.content != null">
+        content = #{record.content,jdbcType=LONGVARCHAR},
+      </if>
+    </set>
+    <if test="_parameter != null">
+      <include refid="Update_By_Example_Where_Clause" />
+    </if>
+  </update>
+  <update id="updateByExampleWithBLOBs" parameterType="map">
+    update notification
+    set id = #{record.id,jdbcType=BIGINT},
+      `type` = #{record.type,jdbcType=VARCHAR},
+      receiver = #{record.receiver,jdbcType=VARCHAR},
+      title = #{record.title,jdbcType=VARCHAR},
+      `status` = #{record.status,jdbcType=VARCHAR},
+      create_time = #{record.createTime,jdbcType=BIGINT},
+      content = #{record.content,jdbcType=LONGVARCHAR}
+    <if test="_parameter != null">
+      <include refid="Update_By_Example_Where_Clause" />
+    </if>
+  </update>
+  <update id="updateByExample" parameterType="map">
+    update notification
+    set id = #{record.id,jdbcType=BIGINT},
+      `type` = #{record.type,jdbcType=VARCHAR},
+      receiver = #{record.receiver,jdbcType=VARCHAR},
+      title = #{record.title,jdbcType=VARCHAR},
+      `status` = #{record.status,jdbcType=VARCHAR},
+      create_time = #{record.createTime,jdbcType=BIGINT}
+    <if test="_parameter != null">
+      <include refid="Update_By_Example_Where_Clause" />
+    </if>
+  </update>
+  <update id="updateByPrimaryKeySelective" parameterType="io.metersphere.base.domain.Notification">
+    update notification
+    <set>
+      <if test="type != null">
+        `type` = #{type,jdbcType=VARCHAR},
+      </if>
+      <if test="receiver != null">
+        receiver = #{receiver,jdbcType=VARCHAR},
+      </if>
+      <if test="title != null">
+        title = #{title,jdbcType=VARCHAR},
+      </if>
+      <if test="status != null">
+        `status` = #{status,jdbcType=VARCHAR},
+      </if>
+      <if test="createTime != null">
+        create_time = #{createTime,jdbcType=BIGINT},
+      </if>
+      <if test="content != null">
+        content = #{content,jdbcType=LONGVARCHAR},
+      </if>
+    </set>
+    where id = #{id,jdbcType=BIGINT}
+  </update>
+  <update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.base.domain.Notification">
+    update notification
+    set `type` = #{type,jdbcType=VARCHAR},
+      receiver = #{receiver,jdbcType=VARCHAR},
+      title = #{title,jdbcType=VARCHAR},
+      `status` = #{status,jdbcType=VARCHAR},
+      create_time = #{createTime,jdbcType=BIGINT},
+      content = #{content,jdbcType=LONGVARCHAR}
+    where id = #{id,jdbcType=BIGINT}
+  </update>
+  <update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.Notification">
+    update notification
+    set `type` = #{type,jdbcType=VARCHAR},
+      receiver = #{receiver,jdbcType=VARCHAR},
+      title = #{title,jdbcType=VARCHAR},
+      `status` = #{status,jdbcType=VARCHAR},
+      create_time = #{createTime,jdbcType=BIGINT}
+    where id = #{id,jdbcType=BIGINT}
+  </update>
+</mapper>
\ No newline at end of file
diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtNotificationMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtNotificationMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..108a8541c8f63d1f4ecbabbf8a8f6bb8a65c7649
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtNotificationMapper.java
@@ -0,0 +1,21 @@
+package io.metersphere.base.mapper.ext;
+
+
+import io.metersphere.base.domain.Notification;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface ExtNotificationMapper {
+
+    Notification getNotification(@Param("id") Integer id, @Param("receiver") String receiver);
+
+    List<Notification> listNotification(@Param("search") String search, @Param("receiver") String receiver);
+
+    List<Notification> listReadNotification(@Param("search") String search, @Param("receiver") String receiver);
+
+    List<Notification> listUnreadNotification(@Param("search") String search, @Param("receiver") String receiver);
+
+    int countNotification(@Param("notification") Notification notification);
+
+}
diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtNotificationMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtNotificationMapper.xml
new file mode 100644
index 0000000000000000000000000000000000000000..027cc9e61fa2f650d93ebb573eabd33838a0cab2
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtNotificationMapper.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="io.metersphere.base.mapper.ext.ExtNotificationMapper">
+
+    <select id="getNotification" resultMap="io.metersphere.base.mapper.NotificationMapper.ResultMapWithBLOBs">
+        select * from notification
+        where id = #{id} and receiver = #{receiver}
+        limit 1
+    </select>
+
+    <select id="listNotification" resultMap="io.metersphere.base.mapper.NotificationMapper.ResultMapWithBLOBs">
+        select * from notification
+        where receiver = #{receiver}
+        <if test='search != null and search != ""'>
+            and ( title like #{search} or content like #{search} )
+        </if>
+        order by create_time desc
+    </select>
+
+    <select id="listReadNotification" resultMap="io.metersphere.base.mapper.NotificationMapper.ResultMapWithBLOBs">
+        select * from notification
+        where receiver = #{receiver} and status = 'READ'
+        <if test='search != null and search != ""'>
+            and ( title like #{search} or content like #{search} )
+        </if>
+        order by create_time desc
+    </select>
+
+    <select id="listUnreadNotification" resultMap="io.metersphere.base.mapper.NotificationMapper.ResultMapWithBLOBs">
+        select * from notification
+        where receiver = #{receiver} and status = 'UNREAD'
+        <if test='search != null and search != ""'>
+            and ( title like #{search} or content like #{search} )
+        </if>
+        order by create_time desc
+    </select>
+
+    <select id="countNotification" resultType="java.lang.Integer">
+        select count(*) from notification
+        where receiver = #{notification.receiver}
+        <if test="notification.type != null">
+            and type = #{notification.type}
+        </if>
+        <if test="notification.status != null">
+            and status = #{notification.status}
+        </if>
+        <if test="notification.uuid != null">
+            and uuid = #{notification.uuid}
+        </if>
+    </select>
+
+
+</mapper>
\ No newline at end of file
diff --git a/backend/src/main/java/io/metersphere/commons/constants/NotificationConstants.java b/backend/src/main/java/io/metersphere/commons/constants/NotificationConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..a8574a5068cf53417f00f234d7be208383849a2c
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/commons/constants/NotificationConstants.java
@@ -0,0 +1,12 @@
+package io.metersphere.commons.constants;
+
+public class NotificationConstants {
+
+    public enum Type {
+        MESSAGE, ANNOUNCEMENT
+    }
+
+    public enum Status {
+        READ, UNREAD
+    }
+}
diff --git a/backend/src/main/java/io/metersphere/commons/utils/SessionUtils.java b/backend/src/main/java/io/metersphere/commons/utils/SessionUtils.java
index 8b81a9e3e24e7193630837e4789d77448fd93162..c8ab04c11565008fa190b72be54630067057393f 100644
--- a/backend/src/main/java/io/metersphere/commons/utils/SessionUtils.java
+++ b/backend/src/main/java/io/metersphere/commons/utils/SessionUtils.java
@@ -7,7 +7,10 @@ import org.apache.shiro.session.Session;
 import org.apache.shiro.session.mgt.DefaultSessionManager;
 import org.apache.shiro.subject.Subject;
 import org.apache.shiro.subject.support.DefaultSubjectContext;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Collection;
 
 import static io.metersphere.commons.constants.SessionConstants.ATTR_USER;
@@ -65,14 +68,26 @@ public class SessionUtils {
     }
 
     public static String getCurrentWorkspaceId() {
+        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
+        if (request.getHeader("WORKSPACE_ID") != null) {
+            return request.getHeader("WORKSPACE_ID");
+        }
         return getUser().getLastWorkspaceId();
     }
 
     public static String getCurrentOrganizationId() {
+        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
+        if (request.getHeader("ORGANIZATION_ID") != null) {
+            return request.getHeader("ORGANIZATION_ID");
+        }
         return getUser().getLastOrganizationId();
     }
 
     public static String getCurrentProjectId() {
+        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
+        if (request.getHeader("PROJECT_ID") != null) {
+            return request.getHeader("PROJECT_ID");
+        }
         return getUser().getLastProjectId();
     }
 }
diff --git a/backend/src/main/java/io/metersphere/notice/sender/SendNoticeAspect.java b/backend/src/main/java/io/metersphere/notice/sender/SendNoticeAspect.java
index 0f551bb08d0ae97c35859d241104dec4722b2080..7b39c0f8db096ba1272e5751b6f4ffb8aea4a87f 100644
--- a/backend/src/main/java/io/metersphere/notice/sender/SendNoticeAspect.java
+++ b/backend/src/main/java/io/metersphere/notice/sender/SendNoticeAspect.java
@@ -35,10 +35,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-
-/**
- * 系统日志:切面处理类
- */
 @Aspect
 @Component
 public class SendNoticeAspect {
diff --git a/backend/src/main/java/io/metersphere/notice/service/NoticeSendService.java b/backend/src/main/java/io/metersphere/notice/service/NoticeSendService.java
index dcdb601d0edfc1ad9eccb55c2337777584dce172..0ef42fbdc0df0862d4626df691820c7d997b7516 100644
--- a/backend/src/main/java/io/metersphere/notice/service/NoticeSendService.java
+++ b/backend/src/main/java/io/metersphere/notice/service/NoticeSendService.java
@@ -1,19 +1,27 @@
 package io.metersphere.notice.service;
 
 import com.alibaba.nacos.client.utils.StringUtils;
+import io.metersphere.base.domain.User;
 import io.metersphere.commons.constants.NoticeConstants;
 import io.metersphere.commons.utils.LogUtil;
+import io.metersphere.commons.utils.SessionUtils;
+import io.metersphere.controller.request.organization.QueryOrgMemberRequest;
 import io.metersphere.notice.domain.MessageDetail;
+import io.metersphere.notice.sender.AbstractNoticeSender;
 import io.metersphere.notice.sender.NoticeModel;
-import io.metersphere.notice.sender.NoticeSender;
 import io.metersphere.notice.sender.impl.DingNoticeSender;
 import io.metersphere.notice.sender.impl.LarkNoticeSender;
 import io.metersphere.notice.sender.impl.MailNoticeSender;
 import io.metersphere.notice.sender.impl.WeComNoticeSender;
+import io.metersphere.service.UserService;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.RegExUtils;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
 import java.util.List;
+import java.util.Map;
 
 @Component
 public class NoticeSendService {
@@ -27,9 +35,13 @@ public class NoticeSendService {
     private LarkNoticeSender larkNoticeSender;
     @Resource
     private NoticeService noticeService;
+    @Resource
+    private NotificationService notificationService;
+    @Resource
+    private UserService userService;
 
-    private NoticeSender getNoticeSender(MessageDetail messageDetail) {
-        NoticeSender noticeSender = null;
+    private AbstractNoticeSender getNoticeSender(MessageDetail messageDetail) {
+        AbstractNoticeSender noticeSender = null;
         switch (messageDetail.getType()) {
             case NoticeConstants.Type.EMAIL:
                 noticeSender = mailNoticeSender;
@@ -50,11 +62,11 @@ public class NoticeSendService {
     }
 
     public void send(String taskType, NoticeModel noticeModel) {
-        String loadReportId = (String) noticeModel.getParamMap().get("id");
         try {
             List<MessageDetail> messageDetails;
             switch (taskType) {
                 case NoticeConstants.Mode.API:
+                    String loadReportId = (String) noticeModel.getParamMap().get("id");
                     messageDetails = noticeService.searchMessageByTypeBySend(NoticeConstants.TaskType.JENKINS_TASK, loadReportId);
                     break;
                 case NoticeConstants.Mode.SCHEDULE:
@@ -64,13 +76,46 @@ public class NoticeSendService {
                     messageDetails = noticeService.searchMessageByType(taskType);
                     break;
             }
-            messageDetails.forEach(messageDetail -> {
-                if (StringUtils.equals(messageDetail.getEvent(), noticeModel.getEvent())) {
-                    this.getNoticeSender(messageDetail).send(messageDetail, noticeModel);
-                }
-            });
+            QueryOrgMemberRequest request = new QueryOrgMemberRequest();
+            request.setOrganizationId(SessionUtils.getCurrentOrganizationId());
+            List<User> orgAllMember = userService.getOrgAllMember(request);
+
+
+            // 异步发送实体通知
+            messageDetails.stream()
+                    .filter(messageDetail -> StringUtils.equals(messageDetail.getEvent(), noticeModel.getEvent()))
+                    .forEach(messageDetail -> this.getNoticeSender(messageDetail).send(messageDetail, noticeModel));
+
+            // 异步发送站内通知
+            sendAnnouncement(noticeModel, orgAllMember);
         } catch (Exception e) {
             LogUtil.error(e.getMessage(), e);
         }
     }
+
+    @Async
+    public void sendAnnouncement(NoticeModel noticeModel, List<User> orgAllMember) {
+        // 替换变量
+        noticeModel.setContext(getContent(noticeModel));
+        orgAllMember.forEach(receiver -> {
+            String context = noticeModel.getContext();
+            LogUtil.debug("发送站内通知: {}, 内容: {}", receiver.getName(), context);
+            notificationService.sendAnnouncement(noticeModel.getSubject(), context, receiver.getId());
+        });
+    }
+
+    private String getContent(NoticeModel noticeModel) {
+        String template = noticeModel.getContext();
+        Map<String, Object> paramMap = noticeModel.getParamMap();
+        if (MapUtils.isNotEmpty(paramMap)) {
+            for (String k : paramMap.keySet()) {
+                if (paramMap.get(k) != null) {
+                    template = RegExUtils.replaceAll(template, "\\$\\{" + k + "}", paramMap.get(k).toString());
+                } else {
+                    template = RegExUtils.replaceAll(template, "\\$\\{" + k + "}", "");
+                }
+            }
+        }
+        return template;
+    }
 }
diff --git a/backend/src/main/java/io/metersphere/notice/service/NotificationService.java b/backend/src/main/java/io/metersphere/notice/service/NotificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e82da315c4343a7f2c7fe8d0532a2e1f6e4e64c
--- /dev/null
+++ b/backend/src/main/java/io/metersphere/notice/service/NotificationService.java
@@ -0,0 +1,88 @@
+package io.metersphere.notice.service;
+
+
+import io.metersphere.base.domain.Notification;
+import io.metersphere.base.domain.NotificationExample;
+import io.metersphere.base.mapper.NotificationMapper;
+import io.metersphere.base.mapper.ext.ExtNotificationMapper;
+import io.metersphere.commons.constants.NotificationConstants;
+import io.metersphere.commons.utils.SessionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class NotificationService {
+
+    @Resource
+    private NotificationMapper notificationMapper;
+
+    @Resource
+    private ExtNotificationMapper extNotificationMapper;
+
+    public void sendAnnouncement(String subject, String content, String receiver) {
+        Notification notification = new Notification();
+        notification.setTitle(subject);
+        notification.setContent(content);
+        notification.setType(NotificationConstants.Type.ANNOUNCEMENT.name());
+        notification.setStatus(NotificationConstants.Status.UNREAD.name());
+        notification.setCreateTime(System.currentTimeMillis());
+        notification.setReceiver(receiver);
+        notificationMapper.insert(notification);
+    }
+
+    public Notification getNotification(int id) {
+        return extNotificationMapper.getNotification(id, SessionUtils.getUser().getId());
+    }
+
+    public int readAll() {
+        Notification record = new Notification();
+        record.setStatus(NotificationConstants.Status.READ.name());
+        NotificationExample example = new NotificationExample();
+        example.createCriteria().andReceiverEqualTo(SessionUtils.getUser().getId());
+        return notificationMapper.updateByExampleSelective(record, example);
+    }
+
+    public int countNotification(Notification notification) {
+        notification.setReceiver(SessionUtils.getUser().getId());
+        return extNotificationMapper.countNotification(notification);
+    }
+
+    public int read(long id) {
+        Notification record = new Notification();
+        record.setStatus(NotificationConstants.Status.READ.name());
+        NotificationExample example = new NotificationExample();
+        example.createCriteria().andIdEqualTo(id).andReceiverEqualTo(SessionUtils.getUser().getId());
+        return notificationMapper.updateByExampleSelective(record, example);
+    }
+
+    public List<Notification> listNotification(Notification notification) {
+        String search = null;
+        if (StringUtils.isNotBlank(notification.getTitle())) {
+            search = "%" + notification.getTitle() + "%";
+        }
+        return extNotificationMapper.listNotification(search, SessionUtils.getUser().getId());
+    }
+
+    public List<Notification> listReadNotification(Notification notification) {
+        String search = null;
+        if (StringUtils.isNotBlank(notification.getTitle())) {
+            search = "%" + notification.getTitle() + "%";
+        }
+        return extNotificationMapper.listReadNotification(search, SessionUtils.getUser().getId());
+    }
+
+    public List<Notification> listUnreadNotification(Notification notification) {
+        String search = null;
+        if (StringUtils.isNotBlank(notification.getTitle())) {
+            search = "%" + notification.getTitle() + "%";
+        }
+        return extNotificationMapper.listUnreadNotification(search, SessionUtils.getUser().getId());
+    }
+
+
+}
diff --git a/backend/src/main/java/io/metersphere/track/service/TestPlanService.java b/backend/src/main/java/io/metersphere/track/service/TestPlanService.java
index 4142b579d01761f5a42deba3cbe056c0b6bbf26a..aaf30666a1b845c4acb769a930bdb6088cb0ffea 100644
--- a/backend/src/main/java/io/metersphere/track/service/TestPlanService.java
+++ b/backend/src/main/java/io/metersphere/track/service/TestPlanService.java
@@ -1007,6 +1007,8 @@ public class TestPlanService {
             performanceRequest.setTestPlanLoadId(caseID);
             if (StringUtils.equals(ReportTriggerMode.API.name(), triggerMode)) {
                 performanceRequest.setTriggerMode(ReportTriggerMode.TEST_PLAN_API.name());
+            } else if (StringUtils.equals(ReportTriggerMode.MANUAL.name(), triggerMode)) {
+                performanceRequest.setTriggerMode(ReportTriggerMode.MANUAL.name());
             } else {
                 performanceRequest.setTriggerMode(ReportTriggerMode.TEST_PLAN_SCHEDULE.name());
             }
diff --git a/backend/src/main/resources/db/migration/V93__v1.12_release.sql b/backend/src/main/resources/db/migration/V93__v1.12_release.sql
index 1908b2d714a674b4ec9b6919f51303f3b29d3453..591ed5a5a5ce474bbb9e0250bb650b2213d95173 100644
--- a/backend/src/main/resources/db/migration/V93__v1.12_release.sql
+++ b/backend/src/main/resources/db/migration/V93__v1.12_release.sql
@@ -68,3 +68,21 @@ CREATE TABLE `plugin` (
 )  ENGINE = InnoDB
     DEFAULT CHARSET = utf8mb4
     COLLATE utf8mb4_general_ci;
+    
+ALTER TABLE test_plan
+    ADD report_summary TEXT NULL COMMENT '测试计划报告总结';
+
+CREATE TABLE IF NOT EXISTS `notification`
+(
+    `id`          BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type`        VARCHAR(30)  DEFAULT NULL COMMENT '通知类型',
+    `receiver`    VARCHAR(100) DEFAULT NULL COMMENT '接收人',
+    `title`       VARCHAR(100) DEFAULT NULL COMMENT '标题',
+    `content`     LONGTEXT COMMENT '内容',
+    `status`      VARCHAR(30)  DEFAULT NULL COMMENT '状态',
+    `create_time` BIGINT(13)   DEFAULT NULL COMMENT 'æ›´æ–°æ—¶é—´',
+    PRIMARY KEY (`id`),
+    KEY `IDX_RECEIVER` (`receiver`) USING BTREE
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE utf8mb4_general_ci;
diff --git a/frontend/src/business/App.vue b/frontend/src/business/App.vue
index 1d276c522d09a743d2d45dd9a54b5a724df946a8..92d844e4037e9827b6cc92c52c422b726ebec830 100644
--- a/frontend/src/business/App.vue
+++ b/frontend/src/business/App.vue
@@ -17,6 +17,7 @@
         <ms-language-switch :color="color"/>
         <ms-header-org-ws :color="color"/>
         <ms-task-center :color="color"/>
+        <ms-notice-center :color="color"/>
       </el-col>
     </el-row>
 
@@ -36,6 +37,7 @@ import {hasLicense, saveLocalStorage, setColor, setDefaultTheme} from "@/common/
 import {registerRequestHeaders} from "@/common/js/ajax";
 import {ORIGIN_COLOR} from "@/common/js/constants";
 import MsTaskCenter from "@/business/components/task/TaskCenter";
+import MsNoticeCenter from "@/business/components/notice/NoticeCenter";
 const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
 const header = requireComponent.keys().length > 0 ? requireComponent("./license/LicenseMessage.vue") : {};
 const display = requireComponent.keys().length > 0 ? requireComponent("./display/Display.vue") : {};
@@ -181,6 +183,7 @@ export default {
     }
   },
   components: {
+    MsNoticeCenter,
     MsTaskCenter,
     MsLanguageSwitch,
     MsUser,
diff --git a/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue b/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue
index 72a4b258fa8881012fc9efcbada550c3212b64ac..30f1efcaf8a4f215989d05ffa6883e4773c5d333 100644
--- a/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue
+++ b/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue
@@ -5,7 +5,7 @@
       @isApiListEnableChange="isApiListEnableChange">
 
       <ms-environment-select :project-id="projectId" v-if="isTestPlan" :is-read-only="isReadOnly"
-                             @setEnvironment="setEnvironment"/>
+                             @setEnvironment="setEnvironment" ref="msEnvironmentSelect"/>
 
       <el-input :placeholder="$t('commons.search_by_name_or_id')" @blur="initTable" class="search-input" size="small"
                 @keyup.enter.native="initTable" v-model="condition.name"/>
@@ -374,6 +374,13 @@
         if (this.$refs.apitable) {
           this.$refs.apitable.clear();
         }
+      },
+      clearEnvAndSelect() {
+        this.environmentId = "";
+        if (this.$refs.msEnvironmentSelect) {
+          this.$refs.msEnvironmentSelect.environmentId = "";
+        }
+        this.clear();
       }
     },
   }
diff --git a/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue b/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue
index 21d90fd9d4eb140d785ea717e3400b05aeaba1b5..2b46b5a39e2d0728a4ef4bc6df09cde074ccac44 100644
--- a/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue
+++ b/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue
@@ -5,7 +5,7 @@
       @isApiListEnableChange="isApiListEnableChange">
 
       <ms-environment-select :project-id="projectId" v-if="isTestPlan" :is-read-only="isReadOnly"
-                             @setEnvironment="setEnvironment"/>
+                             @setEnvironment="setEnvironment" ref="msEnvironmentSelect"/>
 
       <el-input :placeholder="$t('commons.search_by_name_or_id')" @blur="initTable"
                 @keyup.enter.native="initTable" class="search-input" size="small" v-model="condition.name"/>
@@ -241,6 +241,13 @@ export default {
         this.$refs.table.clear();
       }
     },
+    clearEnvAndSelect() {
+      this.environmentId = "";
+      if (this.$refs.msEnvironmentSelect) {
+        this.$refs.msEnvironmentSelect.environmentId = "";
+      }
+      this.clear();
+    },
     showExecResult(row) {
       this.visible = false;
       this.$emit('showExecResult', row);
diff --git a/frontend/src/business/components/notice/NoticeCenter.vue b/frontend/src/business/components/notice/NoticeCenter.vue
new file mode 100644
index 0000000000000000000000000000000000000000..afe47ad3f539979d7fd0bfda49e6cb388e032b7a
--- /dev/null
+++ b/frontend/src/business/components/notice/NoticeCenter.vue
@@ -0,0 +1,442 @@
+<template>
+  <div>
+    <el-menu :unique-opened="true" class="header-user-menu align-right header-top-menu"
+             mode="horizontal"
+             :background-color="color"
+             text-color="#fff"
+             active-text-color="#fff">
+      <el-menu-item onselectstart="return false">
+        <el-tooltip effect="light">
+          <template v-slot:content>
+            <span>{{ $t('commons.notice_center') }}</span>
+          </template>
+          <div @click="showNoticeCenter" v-if="runningTotal > 0">
+            <el-badge :value="runningTotal" class="item" type="primary">
+              <font-awesome-icon class="icon global focusing" :icon="['fas', 'bell']"/>
+            </el-badge>
+          </div>
+          <font-awesome-icon @click="showNoticeCenter" class="icon global focusing" :icon="['fas', 'bell']" v-else/>
+        </el-tooltip>
+      </el-menu-item>
+    </el-menu>
+
+    <el-drawer :visible.sync="taskVisible" :destroy-on-close="true" direction="rtl"
+               :withHeader="true" :modal="false" :title="$t('commons.task_center')" size="600px"
+               custom-class="ms-drawer-task">
+      <div style="color: #2B415C;margin: 0px 20px 0px">
+        <el-form label-width="68px">
+          <el-row>
+            <el-col :span="8">
+              <el-form-item :label="$t('test_track.report.list.trigger_mode')" prop="runMode">
+                <el-select size="small" style="margin-right: 10px" v-model="condition.triggerMode" @change="init">
+                  <el-option v-for="item in runMode" :key="item.id" :value="item.id" :label="item.label"/>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item :label="$t('commons.status')" prop="status">
+                <el-select size="small" style="margin-right: 10px" v-model="condition.executionStatus" @change="init">
+                  <el-option v-for="item in runStatus" :key="item.id" :value="item.id" :label="item.label"/>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item :label="$t('commons.executor')" prop="status">
+                <el-select v-model="condition.executor" :placeholder="$t('commons.executor')" filterable size="small"
+                           style="margin-right: 10px" @change="init">
+                  <el-option
+                    v-for="item in maintainerOptions"
+                    :key="item.id"
+                    :label="item.id + ' (' + item.name + ')'"
+                    :value="item.id">
+                  </el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </div>
+
+      <div class="report-container">
+        <div v-for="item in taskData" :key="item.id" style="margin-bottom: 5px">
+          <el-card class="ms-card-task" @click.native="showReport(item,$event)">
+            <span><el-link type="primary">{{ getModeName(item.executionModule) }} </el-link>: {{
+                item.name
+              }} </span><br/>
+            <span>
+              执行器:{{ item.actuator }} 由 {{ item.executor }}
+              {{ item.executionTime | timestampFormatDate }}
+              {{ getMode(item.triggerMode) }}
+            </span>
+            <br/>
+            <el-row>
+              <el-col :span="20">
+                <el-progress :percentage="getPercentage(item.executionStatus)" :format="format"/>
+              </el-col>
+              <el-col :span="4">
+                  <span v-if="item.executionStatus && item.executionStatus.toLowerCase() === 'error'"
+                        class="ms-task-error">
+                     error
+                  </span>
+                <span v-else-if="item.executionStatus && item.executionStatus.toLowerCase() === 'success'"
+                      class="ms-task-success">
+                     success
+                </span>
+                <span v-else>{{
+                    item.executionStatus ? item.executionStatus.toLowerCase() : item.executionStatus
+                  }}</span>
+              </el-col>
+            </el-row>
+          </el-card>
+        </div>
+      </div>
+    </el-drawer>
+
+  </div>
+</template>
+
+<script>
+import MsDrawer from "../common/components/MsDrawer";
+import {getCurrentProjectID, getCurrentUser, hasPermissions} from "@/common/js/utils";
+import MsRequestResultTail from "../../components/api/definition/components/response/RequestResultTail";
+
+export default {
+  name: "MsNoticeCenter",
+  components: {
+    MsDrawer,
+    MsRequestResultTail
+  },
+  inject: [
+    'reload'
+  ],
+  data() {
+    return {
+      runningTotal: 0,
+      taskVisible: false,
+      result: {},
+      taskData: [],
+      response: {},
+      initEnd: false,
+      visible: false,
+      showType: "",
+      runMode: [
+        {id: '', label: this.$t('api_test.definition.document.data_set.all')},
+        {id: 'BATCH', label: this.$t('api_test.automation.batch_execute')},
+        {id: 'SCHEDULE', label: this.$t('commons.trigger_mode.schedule')},
+        {id: 'MANUAL', label: this.$t('commons.trigger_mode.manual')},
+        {id: 'API', label: 'API'}
+      ],
+      runStatus: [
+        {id: '', label: this.$t('api_test.definition.document.data_set.all')},
+        {id: 'Saved', label: 'Saved'},
+        {id: 'Starting', label: 'Starting'},
+        {id: 'Running', label: 'Running'},
+        {id: 'Reporting', label: 'Reporting'},
+        {id: 'Completed', label: 'Completed'},
+        {id: 'error', label: 'Error'},
+        {id: 'success', label: 'Success'}
+      ],
+      condition: {triggerMode: "", executionStatus: ""},
+      maintainerOptions: [],
+      websocket: Object,
+    };
+  },
+  props: {
+    color: String
+  },
+  created() {
+    if (hasPermissions('PROJECT_API_SCENARIO:READ')) {
+      this.condition.executor = getCurrentUser().id;
+    }
+  },
+  watch: {
+    taskVisible(v) {
+      if (!v) {
+        this.close();
+      }
+    }
+  },
+  methods: {
+    format(item) {
+      return '';
+    },
+    getMaintainerOptions() {
+      this.$post('/user/project/member/tester/list', {projectId: getCurrentProjectID()}, response => {
+        this.maintainerOptions = response.data;
+        this.condition.executor = getCurrentUser().id;
+      });
+    },
+    initWebSocket() {
+      let protocol = "ws://";
+      if (window.location.protocol === 'https:') {
+        protocol = "wss://";
+      }
+      const uri = protocol + window.location.host + "/task/center/count/running/" + getCurrentProjectID();
+      this.websocket = new WebSocket(uri);
+      this.websocket.onmessage = this.onMessage;
+      this.websocket.onopen = this.onOpen;
+      this.websocket.onerror = this.onError;
+      this.websocket.onclose = this.onClose;
+    },
+    onOpen() {
+    },
+    onError(e) {
+    },
+    onMessage(e) {
+      let taskTotal = e.data;
+      this.runningTotal = taskTotal;
+      this.initIndex++;
+      if (this.taskVisible && taskTotal > 0 && this.initEnd) {
+        setTimeout(() => {
+          this.initEnd = false;
+          this.init();
+        }, 3000);
+      }
+    },
+    onClose(e) {
+    },
+    showNoticeCenter() {
+      this.getTaskRunning();
+      this.getMaintainerOptions();
+      this.init();
+      this.taskVisible = true;
+    },
+    close() {
+      this.visible = false;
+      this.taskVisible = false;
+      this.showType = "";
+      if (this.websocket && this.websocket.close instanceof Function) {
+        this.websocket.close();
+      }
+    },
+    open() {
+      this.showNoticeCenter();
+      this.initIndex = 0;
+    },
+    getPercentage(status) {
+      if (status) {
+        status = status.toLowerCase();
+        if (status === "waiting") {
+          return 0;
+        }
+        if (status === 'saved' || status === 'completed' || status === 'success' || status === 'error') {
+          return 100;
+        }
+      }
+      return 60;
+    },
+    getModeName(executionModule) {
+      switch (executionModule) {
+        case "SCENARIO":
+          return this.$t('test_track.scenario_test_case');
+        case "PERFORMANCE":
+          return this.$t('test_track.performance_test_case');
+        case "API":
+          return this.$t('test_track.api_test_case');
+      }
+    },
+    showReport(row, env) {
+      let status = row.executionStatus;
+      if (status) {
+        status = row.executionStatus.toLowerCase();
+        if (status === 'saved' || status === 'completed' || status === 'success' || status === 'error') {
+          this.taskVisible = false;
+          switch (row.executionModule) {
+            case "SCENARIO":
+              this.$router.push({
+                path: '/api/automation/report/view/' + row.id,
+              });
+              break;
+            case "PERFORMANCE":
+              this.$router.push({
+                path: '/performance/report/view/' + row.id,
+              });
+              break;
+            case "API":
+              this.getExecResult(row.id);
+              break;
+          }
+        } else {
+          this.$warning("正在运行中,请稍后查看");
+        }
+      }
+    },
+
+    getExecResult(reportId) {
+      if (reportId) {
+        let url = "/api/definition/report/get/" + reportId;
+        this.$get(url, response => {
+          if (response.data) {
+            let data = JSON.parse(response.data.content);
+            this.response = data;
+            this.visible = true;
+          }
+        });
+      }
+    },
+    getMode(mode) {
+      if (mode === 'MANUAL') {
+        return this.$t('commons.trigger_mode.manual');
+      }
+      if (mode === 'SCHEDULE') {
+        return this.$t('commons.trigger_mode.schedule');
+      }
+      if (mode === 'TEST_PLAN_SCHEDULE') {
+        return this.$t('commons.trigger_mode.schedule');
+      }
+      if (mode === 'API') {
+        return this.$t('commons.trigger_mode.api');
+      }
+      if (mode === 'BATCH') {
+        return this.$t('api_test.automation.batch_execute');
+      }
+      return mode;
+    },
+    getTaskRunning() {
+      this.initWebSocket();
+    },
+    calculationRunningTotal() {
+      if (this.taskData) {
+        let total = 0;
+        this.taskData.forEach(item => {
+          if (this.getPercentage(item.executionStatus) !== 100) {
+            total++;
+          }
+        });
+        this.runningTotal = total;
+      }
+    },
+    init() {
+      if (this.showType === "CASE" || this.showType === "SCENARIO") {
+        return;
+      }
+      this.result.loading = true;
+      this.condition.projectId = getCurrentProjectID();
+      this.result = this.$post('/task/center/list', this.condition, response => {
+        this.taskData = response.data;
+        this.calculationRunningTotal();
+        this.initEnd = true;
+      });
+    },
+    initCaseHistory(id) {
+      this.result = this.$get('/task/center/case/' + id, response => {
+        this.taskData = response.data;
+      });
+    },
+    openHistory(id) {
+      this.initCaseHistory(id);
+      this.taskVisible = true;
+      this.showType = "CASE";
+    },
+    openScenarioHistory(id) {
+      this.result = this.$get('/task/center/scenario/' + id, response => {
+        this.taskData = response.data;
+      });
+      this.showType = "SCENARIO";
+      this.taskVisible = true;
+    }
+  }
+};
+</script>
+
+<style>
+.ms-drawer-task {
+  top: 42px !important;
+}
+</style>
+
+<style scoped>
+.el-icon-check {
+  color: #44b349;
+  margin-left: 10px;
+}
+
+.report-container {
+  height: calc(100vh - 180px);
+  min-height: 600px;
+  overflow-y: auto;
+}
+
+.align-right {
+  float: right;
+}
+
+.icon {
+  width: 24px;
+}
+
+/deep/ .el-drawer__header {
+  font-size: 18px;
+  color: #0a0a0a;
+  border-bottom: 1px solid #E6E6E6;
+  background-color: #FFF;
+  margin-bottom: 10px;
+  padding: 10px;
+}
+
+.ms-card-task >>> .el-card__body {
+  padding: 10px;
+}
+
+.global {
+  color: #fff;
+}
+
+.header-top-menu {
+  height: 40px;
+  line-height: 40px;
+  color: inherit;
+}
+
+.header-top-menu.el-menu--horizontal > li {
+  height: 40px;
+  line-height: 40px;
+  color: inherit;
+}
+
+.header-top-menu.el-menu--horizontal > li.el-submenu > * {
+  height: 39px;
+  line-height: 40px;
+  color: inherit;
+}
+
+.header-top-menu.el-menu--horizontal > li.is-active {
+  background: var(--color_shallow) !important;
+}
+
+.ms-card-task:hover {
+  cursor: pointer;
+  border-color: #783887;
+}
+
+/deep/ .el-progress-bar {
+  padding-right: 20px;
+}
+
+/deep/ .el-menu-item {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+/deep/ .el-badge__content.is-fixed {
+  top: 25px;
+}
+
+/deep/ .el-badge__content {
+  border-radius: 10px;
+  height: 10px;
+  line-height: 10px;
+}
+
+.item {
+  margin-right: 10px;
+}
+
+.ms-task-error {
+  color: #F56C6C;
+}
+
+.ms-task-success {
+  color: #67C23A;
+}
+</style>
diff --git a/frontend/src/business/components/performance/test/EditPerformanceTest.vue b/frontend/src/business/components/performance/test/EditPerformanceTest.vue
index bdba7403dd4372b0407d207be1fc54514f7b7ffe..d4c6ac5fc9f3596065884a6769ab9923ab230c45 100644
--- a/frontend/src/business/components/performance/test/EditPerformanceTest.vue
+++ b/frontend/src/business/components/performance/test/EditPerformanceTest.vue
@@ -280,7 +280,7 @@ export default {
       let options = this.getSaveOption();
 
       this.result = this.$request(options, (response) => {
-        this.test.id = response.data;
+        this.test.id = response.data.id;
         this.$success(this.$t('commons.save_success'));
         this.result = this.$post(this.runPath, {id: this.test.id, triggerMode: 'MANUAL'}, (response) => {
           let reportId = response.data;
diff --git a/frontend/src/business/components/track/plan/components/TestPlanList.vue b/frontend/src/business/components/track/plan/components/TestPlanList.vue
index bb4c60f391e0628342fb977e127ed42f5a154a72..a994d5015c83ed125f83617fa19a125e35cb406f 100644
--- a/frontend/src/business/components/track/plan/components/TestPlanList.vue
+++ b/frontend/src/business/components/track/plan/components/TestPlanList.vue
@@ -499,6 +499,7 @@ export default {
       param.testPlanId = this.currentPlanId;
       param.projectId = getCurrentProjectID();
       param.userId = getCurrentUserId();
+      param.triggerMode = 'MANUAL';
       this.result = this.$post('test/plan/run/', param,() => {
         this.$success(this.$t('commons.run_success'));
       }, () => {
diff --git a/frontend/src/business/components/track/plan/view/comonents/api/TestCaseApiRelevance.vue b/frontend/src/business/components/track/plan/view/comonents/api/TestCaseApiRelevance.vue
index 3792f6b15dbbc15ea78a2635930d4552216349c2..6d6628320d43e26e66700b4d6ee69b59190b746e 100644
--- a/frontend/src/business/components/track/plan/view/comonents/api/TestCaseApiRelevance.vue
+++ b/frontend/src/business/components/track/plan/view/comonents/api/TestCaseApiRelevance.vue
@@ -105,6 +105,13 @@
         }
       },
       setProject(projectId) {
+        // 切换项目 清空环境和选中行
+        if (this.$refs.apiList) {
+          this.$refs.apiList.clearEnvAndSelect();
+        }
+        if (this.$refs.apiCaseList) {
+          this.$refs.apiCaseList.clearEnvAndSelect();
+        }
         this.projectId = projectId;
       },
       isApiListEnableChange(data) {
diff --git a/frontend/src/common/js/ajax.js b/frontend/src/common/js/ajax.js
index 27bd3b2e4faf8097fb62939389455fccf9de3506..6ef98509df7a89853d27bf7421f066e4027d3ec0 100644
--- a/frontend/src/common/js/ajax.js
+++ b/frontend/src/common/js/ajax.js
@@ -2,6 +2,7 @@ import {Message, MessageBox} from 'element-ui';
 import axios from "axios";
 import i18n from '../../i18n/i18n';
 import {TokenKey} from "@/common/js/constants";
+import {getCurrentOrganizationId, getCurrentProjectID, getCurrentWorkspaceId} from "@/common/js/utils";
 
 export function registerRequestHeaders() {
   axios.interceptors.request.use(config => {
@@ -9,6 +10,10 @@ export function registerRequestHeaders() {
     if (user && user.csrfToken) {
       config.headers['CSRF-TOKEN'] = user.csrfToken;
     }
+    // 包含 组织 工作空间 项目的标识
+    config.headers['ORGANIZATION_ID'] = getCurrentOrganizationId();
+    config.headers['WORKSPACE_ID'] = getCurrentWorkspaceId();
+    config.headers['PROJECT_ID'] = getCurrentProjectID();
     return config;
   });
 }
diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js
index 46aae0b8f58879d4b4c15f3f7ae56fd5fd4c511e..4c0169c5d2011d1b8385d03652bf5f3e93c531ae 100644
--- a/frontend/src/i18n/en-US.js
+++ b/frontend/src/i18n/en-US.js
@@ -168,6 +168,7 @@ export default {
     api_case: "Api Case",
     scenario_case: "Scenario Case",
     task_center: "Task center",
+    notice_center: "Notice center",
     all_module_title: "All module",
     create_user: 'Creator',
     run_message: "The task is being executed, please go to the task center to view the details",
diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js
index efcd3dc838347f51d9db5946b288be0633bcff0b..4d262f1767a9c4bc7c2f913f3a7756f202d6e787 100644
--- a/frontend/src/i18n/zh-CN.js
+++ b/frontend/src/i18n/zh-CN.js
@@ -169,6 +169,7 @@ export default {
     api_case: "接口用例",
     scenario_case: "场景用例",
     task_center: "任务中心",
+    notice_center: "消息中心",
     all_module_title: "全部模块",
     create_user: '创建人',
     run_message: "任务执行中,请到任务中心查看详情",
diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js
index 3962fba15e372091282560b6906b87158b2d7e93..7c539a5d74c34e9f69fbcc3dbbfac402808638ec 100644
--- a/frontend/src/i18n/zh-TW.js
+++ b/frontend/src/i18n/zh-TW.js
@@ -169,6 +169,7 @@ export default {
     api_case: "接口用例",
     scenario_case: "場景用例",
     task_center: "任务中心",
+    notice_center: "消息中心",
     all_module_title: "全部模塊",
     create_user: "創建人",
     run_message: "任務執行中,請到任務中心查看詳情",