From 8cc58151079f110c4fe525ff309566fda6ed92c0 Mon Sep 17 00:00:00 2001
From: awu0403 <76416779+awu0403@users.noreply.github.com>
Date: Mon, 21 Apr 2025 13:30:35 +0800
Subject: [PATCH] Send notification to cloud user (#6415)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* send subscription expire notification

* optimize code

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
---
 frontend/src/app.js                           |  2 +
 .../system-user-notification-item.js          | 38 +++++++
 .../components/system-user-notification.js    | 43 ++++++++
 frontend/src/utils/notification-api.js        | 65 ++++++++++++
 seahub/api2/endpoints/admin/organizations.py  | 15 ++-
 .../api2/endpoints/admin/sys_notifications.py | 98 ++++++++++++++++++-
 seahub/api2/endpoints/notifications.py        | 60 +++++++++++-
 seahub/notifications/models.py                | 64 +++++++++++-
 seahub/urls.py                                | 11 ++-
 seahub/utils/ccnet_db.py                      | 12 +++
 sql/mysql.sql                                 | 12 +++
 11 files changed, 411 insertions(+), 9 deletions(-)
 create mode 100644 frontend/src/components/system-user-notification-item.js
 create mode 100644 frontend/src/components/system-user-notification.js
 create mode 100644 frontend/src/utils/notification-api.js

diff --git a/frontend/src/app.js b/frontend/src/app.js
index ebc30d5ca0..eeccc71ba5 100644
--- a/frontend/src/app.js
+++ b/frontend/src/app.js
@@ -8,6 +8,7 @@ import { Utils, isMobile } from './utils/utils';
 import SystemNotification from './components/system-notification';
 import EventBus from './components/common/event-bus';
 import Header from './components/header';
+import SystemUserNotification from './components/system-user-notification';
 import SidePanel from './components/side-panel';
 import ResizeBar from './components/resize-bar';
 import {
@@ -278,6 +279,7 @@ class App extends Component {
     return (
       <React.Fragment>
         <SystemNotification />
+        <SystemUserNotification />
         <Header
           isSidePanelClosed={isSidePanelClosed}
           onCloseSidePanel={this.onCloseSidePanel}
diff --git a/frontend/src/components/system-user-notification-item.js b/frontend/src/components/system-user-notification-item.js
new file mode 100644
index 0000000000..0d95f97f91
--- /dev/null
+++ b/frontend/src/components/system-user-notification-item.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { gettext } from '../utils/constants';
+import { notificationAPI } from '../utils/notification-api';
+import '../css/system-notification.css';
+import PropTypes from 'prop-types';
+
+class SystemUserNotificationItem extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isClosed: false
+    };
+  }
+
+  close = () => {
+    this.setState({ isClosed: true });
+    notificationAPI.setSysUserNotificationToSeen(this.props.notificationID);
+  };
+
+  render() {
+    if (this.state.isClosed) {
+      return null;
+    }
+    return (
+      <div id="info-bar" className="d-flex justify-content-between">
+        <span className="mr-3" aria-hidden="true"></span>
+        <p id="info-bar-info" className="m-0" dangerouslySetInnerHTML={{ __html: this.props.msg }}></p>
+        <button className="close sf2-icon-x1" title={gettext('Close')} aria-label={gettext('Close')} onClick={this.close}></button>
+      </div>
+    );
+  }
+}
+
+SystemUserNotificationItem.propTypes = {
+  msg: PropTypes.string.isRequired,
+  notificationID: PropTypes.number.isRequired,
+};
+export default SystemUserNotificationItem;
diff --git a/frontend/src/components/system-user-notification.js b/frontend/src/components/system-user-notification.js
new file mode 100644
index 0000000000..45cf5e5278
--- /dev/null
+++ b/frontend/src/components/system-user-notification.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import '../css/system-notification.css';
+import SystemUserNotificationItem from './system-user-notification-item';
+import { notificationAPI } from '../utils/notification-api';
+
+
+class SystemUserNotification extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      userNoteMsgs: []
+    };
+  }
+
+  componentDidMount() {
+    notificationAPI.listSysUserUnseenNotifications().then((res) => {
+      this.setState({
+        userNoteMsgs: res.data.notifications
+      });
+    });
+  }
+
+  render() {
+    let { userNoteMsgs } = this.state;
+    if (!userNoteMsgs) {
+      return null;
+    }
+    const userNoteMsgItem = userNoteMsgs.map((item, index) => {
+      return (
+        <SystemUserNotificationItem
+          key={index}
+          notificationItem={item}
+          msg={item.msg_format}
+          notificationID={item.id}
+        />
+      );
+    });
+    return userNoteMsgItem;
+  }
+}
+
+export default SystemUserNotification;
diff --git a/frontend/src/utils/notification-api.js b/frontend/src/utils/notification-api.js
new file mode 100644
index 0000000000..caf0ce9781
--- /dev/null
+++ b/frontend/src/utils/notification-api.js
@@ -0,0 +1,65 @@
+import axios from 'axios';
+import cookie from 'react-cookies';
+import { siteRoot } from './constants';
+
+class NotificationAPI {
+
+  init({ server, username, password, token }) {
+    this.server = server;
+    this.username = username;
+    this.password = password;
+    this.token = token;
+    if (this.token && this.server) {
+      this.req = axios.create({
+        baseURL: this.server,
+        headers: { 'Authorization': 'Token ' + this.token },
+      });
+    }
+    return this;
+  }
+
+  initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
+    if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
+      var server = siteRoot.substring(0, siteRoot.length - 1);
+      this.server = server;
+    } else {
+      this.server = siteRoot;
+    }
+
+    this.req = axios.create({
+      headers: {
+        'X-CSRFToken': xcsrfHeaders,
+      }
+    });
+    return this;
+  }
+
+  _sendPostRequest(url, form) {
+    if (form.getHeaders) {
+      return this.req.post(url, form, {
+        headers: form.getHeaders()
+      });
+    } else {
+      return this.req.post(url, form);
+    }
+  }
+
+  listSysUserUnseenNotifications() {
+    const url = this.server + '/api/v2.1/sys-user-notifications/unseen/';
+    return this.req.get(url);
+
+  }
+
+  setSysUserNotificationToSeen(notificationID) {
+    const url = this.server + 'api/v2.1/sys-user-notifications/' + notificationID + '/seen/';
+    return this.req.put(url);
+  }
+
+
+}
+
+let notificationAPI = new NotificationAPI();
+let xcsrfHeaders = cookie.load('sfcsrftoken');
+notificationAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
+
+export { notificationAPI };
diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py
index f254682c2f..7e380e4f57 100644
--- a/seahub/api2/endpoints/admin/organizations.py
+++ b/seahub/api2/endpoints/admin/organizations.py
@@ -22,10 +22,12 @@ from seahub.base.accounts import User
 from seahub.base.models import OrgLastActivityTime
 from seahub.api2.authentication import TokenAuthentication
 from seahub.api2.throttling import UserRateThrottle
-from seahub.api2.utils import api_error
+from seahub.api2.utils import api_error, to_python_boolean
 from seahub.api2.permissions import IsProVersion
 from seahub.role_permissions.utils import get_available_roles
 from seahub.organizations.models import OrgSAMLConfig
+from seahub.utils.ccnet_db import CcnetDB
+
 
 try:
     from seahub.settings import ORG_MEMBER_QUOTA_ENABLED
@@ -515,7 +517,8 @@ class AdminOrganizationsBaseInfo(APIView):
             error_msg = 'Feature is not enabled.'
             return api_error(status.HTTP_403_FORBIDDEN, error_msg)
 
-        org_ids = request.GET.getlist('org_ids', [])
+        org_ids = request.GET.getlist('org_ids',[])
+        include_org_staffs = to_python_boolean(request.GET.get('include_org_staffs', 'false'))
         orgs = []
         for org_id in org_ids:
             try:
@@ -525,5 +528,13 @@ class AdminOrganizationsBaseInfo(APIView):
             except Exception:
                 continue
             base_info = {'org_id': org.org_id, 'org_name': org.org_name}
+            staffs = []
+            if include_org_staffs:
+                try:
+                    ccnet_db = CcnetDB()
+                    staffs = ccnet_db.get_org_staffs(int(org_id))
+                except Exception:
+                    pass
+                base_info['org_staffs'] = staffs
             orgs.append(base_info)
         return Response({'organization_list': orgs})
diff --git a/seahub/api2/endpoints/admin/sys_notifications.py b/seahub/api2/endpoints/admin/sys_notifications.py
index 18bc2b4b4e..6fb48d0f22 100644
--- a/seahub/api2/endpoints/admin/sys_notifications.py
+++ b/seahub/api2/endpoints/admin/sys_notifications.py
@@ -12,8 +12,9 @@ from seahub.api2.authentication import TokenAuthentication
 from seahub.api2.throttling import UserRateThrottle
 from seahub.api2.utils import api_error
 
-from seahub.notifications.models import Notification
+from seahub.notifications.models import Notification, SysUserNotification
 from seahub.notifications.settings import NOTIFICATION_CACHE_TIMEOUT
+from seahub.base.accounts import User
 
 logger = logging.getLogger(__name__)
 
@@ -168,3 +169,98 @@ class AdminSysNotificationView(APIView):
 
         return Response({'success': True})
 
+
+class AdminSysUserNotificationsView(APIView):
+    """
+    admin notifications of designated user
+    """
+    authentication_classes = (TokenAuthentication, SessionAuthentication)
+    throttle_classes = (UserRateThrottle,)
+    permission_classes = (IsAdminUser,)
+
+    def get(self, request):
+        try:
+            page = int(request.GET.get('page', 1))
+            per_page = int(request.GET.get('per_page', 25))
+        except Exception as e:
+            error_msg = 'per_page or page invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        start, end = (page - 1) * per_page, page * per_page
+        try:
+            notifications = SysUserNotification.objects.all().order_by('-id')
+            notifications_count = notifications.count()
+        except Exception as e:
+            logger.error(e)
+            error_msg = 'Internal Server Error'
+            return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+        return Response({
+            'notifications': [n.to_dict() for n in notifications[start: end]],
+            "total_count": notifications_count
+        })
+
+    def post(self,request):
+        msg = request.data.get('msg', '')
+        username = request.data.get('username', '')
+
+        # arguments check
+        if not msg:
+            error_msg = 'msg invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        if not username:
+            error_msg = 'user invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        # resource check
+        try:
+            User.objects.get(email=username)
+        except User.DoesNotExist:
+            error_msg = 'User %s not found.' % username
+            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+        try:
+            notification = SysUserNotification.objects.create_sys_user_notificatioin(msg, username)
+        except Exception as e:
+            logger.error(e)
+            error_msg = 'Internal Server Error'
+            return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+        return Response({'notification': notification.to_dict()})
+
+class AdminSysUserNotificationView(APIView):
+    authentication_classes = (TokenAuthentication, SessionAuthentication)
+    throttle_classes = (UserRateThrottle,)
+    permission_classes = (IsAdminUser,)
+
+    def delete(self, request, nid):
+        """
+        delete a system-to-user notification
+        Permission checking:
+        1.login and is admin user.
+        """
+
+        try:
+            nid = int(nid)
+        except ValueError:
+            error_msg = 'nid invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        if nid <= 0:
+            error_msg = 'nid invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        notification = SysUserNotification.objects.filter(id=nid).first()
+        if not notification:
+            error_msg = 'notification %s not found.' % nid
+            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+        try:
+            notification.delete()
+        except Exception as e:
+            logger.error(e)
+            error_msg = 'Internal Server Error'
+            return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+        return Response({'success': True})
diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py
index dfc779862d..ec639c429c 100644
--- a/seahub/api2/endpoints/notifications.py
+++ b/seahub/api2/endpoints/notifications.py
@@ -10,7 +10,7 @@ from rest_framework import status
 
 from seahub.api2.authentication import TokenAuthentication
 from seahub.api2.throttling import UserRateThrottle
-from seahub.notifications.models import UserNotification
+from seahub.notifications.models import UserNotification, SysUserNotification
 
 from seahub.notifications.utils import update_notice_detail, update_sdoc_notice_detail
 from seahub.api2.utils import api_error
@@ -370,3 +370,61 @@ class AllNotificationsView(APIView):
 
 
         return Response({'success': True})
+
+class SysUserNotificationUnseenView(APIView):
+    authentication_classes = (TokenAuthentication, SessionAuthentication)
+    permission_classes = (IsAuthenticated,)
+    throttle_classes = (UserRateThrottle,)
+
+    def get(self, request):
+        """
+        get the unseen sys-user-notifications by login user
+        """
+        username = request.user.username
+        notifications = SysUserNotification.objects.unseen_notes(username)
+        return Response({
+            'notifications': [n.to_dict() for n in notifications],
+        })
+
+class SysUserNotificationSeenView(APIView):
+    authentication_classes = (TokenAuthentication, SessionAuthentication)
+    permission_classes = (IsAuthenticated,)
+    throttle_classes = (UserRateThrottle,)
+
+    def put(self, request, nid):
+        """
+        mark a sys-user-notification seen by login user
+        Permission checking:
+        1. login user.
+        """
+        # arguments check
+        username = request.user.username
+        try:
+            nid = int(nid)
+        except ValueError:
+            error_msg = 'nid invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        if nid <= 0:
+            error_msg = 'nid invalid.'
+            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+        # resouce check
+        notification = SysUserNotification.objects.filter(id=nid).first()
+        if not notification:
+            error_msg = 'notification %s not found.' % nid
+            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+        if notification.to_user != username:
+            error_msg = 'Permission denied.'
+            return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+        # set notification to seen
+        try:
+            notification.update_notification_to_seen()
+        except Exception as e:
+            logger.error(e)
+            error_msg = 'Internal Server Error'
+            return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+        return Response({'notification': notification.to_dict()})
diff --git a/seahub/notifications/models.py b/seahub/notifications/models.py
index 0cad4c4f6c..fe4bce80d4 100644
--- a/seahub/notifications/models.py
+++ b/seahub/notifications/models.py
@@ -9,7 +9,7 @@ from django.urls import reverse
 from django.db import models
 from django.conf import settings
 from django.forms import ModelForm, Textarea
-from django.utils.html import escape
+from django.utils.html import escape, urlize
 from django.utils.translation import gettext as _
 from django.core.cache import cache
 from django.template.loader import render_to_string
@@ -17,8 +17,9 @@ from django.template.loader import render_to_string
 from seaserv import seafile_api, ccnet_api
 
 from seahub.base.fields import LowerCaseCharField
-from seahub.base.templatetags.seahub_tags import email2nickname
+from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
 from seahub.invitations.models import Invitation
+from seahub.utils.timeutils import datetime_to_isoformat_timestr
 from seahub.utils import normalize_cache_key, get_site_scheme_and_netloc
 from seahub.constants import HASH_URLS
 from seahub.file_participants.utils import list_file_participants
@@ -42,6 +43,9 @@ class NotificationManager(models.Manager):
 
 ########## system notification
 class Notification(models.Model):
+    """
+        global system notification
+    """
     message = models.CharField(max_length=512)
     primary = models.BooleanField(default=False, db_index=True)
     objects = NotificationManager()
@@ -61,6 +65,62 @@ class NotificationForm(ModelForm):
             'message': Textarea(),
         }
 
+
+class SysUserNotificationManager(models.Manager):
+    def create_sys_user_notificatioin(self, msg, user):
+        notification = self.create(
+            message = msg,
+            to_user = user,
+        )
+        return notification
+
+    def unseen_notes(self, user):
+        notes = self.filter(to_user=user, seen=0)
+        return notes
+
+
+class SysUserNotification(models.Model):
+    """
+    system notification to designated user
+    """
+    message = models.TextField(null=False, blank=False)
+    to_user = models.CharField(max_length=255, db_index=True)
+    seen = models.BooleanField(default=False, db_index=True)
+    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
+    objects = SysUserNotificationManager()
+
+    class Meta:
+        ordering = ["-created_at"]
+
+    def update_notification_to_seen(self):
+        self.seen = True
+        self.save()
+
+    @property
+    def format_msg(self):
+        return urlize(self.message, autoescape=True)
+
+    def to_dict(self):
+        email = self.to_user
+        orgs = ccnet_api.get_orgs_by_user(email)
+        org_name = ''
+        try:
+            if orgs:
+                org_name = orgs[0].org_name
+        except Exception as e:
+            logger.error(e)
+        return {
+            'id': self.id,
+            'msg': self.message,
+            'username': self.to_user,
+            'name': email2nickname(self.to_user),
+            'contact_email': email2contact_email(self.to_user),
+            'seen': self.seen,
+            'org_name': org_name,
+            'created_at': datetime_to_isoformat_timestr(self.created_at),
+            'msg_format': self.format_msg
+        }
+
 ########## user notification
 MSG_TYPE_GROUP_JOIN_REQUEST = 'group_join_request'
 MSG_TYPE_ADD_USER_TO_GROUP = 'add_user_to_group'
diff --git a/seahub/urls.py b/seahub/urls.py
index b0f1a2a0eb..d83d38bc7b 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -92,7 +92,8 @@ from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchV
 from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
 from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView
 from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView
-from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView, AllNotificationsView
+from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView, \
+    SysUserNotificationSeenView, AllNotificationsView, SysUserNotificationUnseenView
 from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
 from seahub.api2.endpoints.user_avatar import UserAvatarView
 from seahub.api2.endpoints.wikis import WikisView, WikiView
@@ -192,7 +193,8 @@ from seahub.api2.endpoints.admin.group_owned_libraries import AdminGroupOwnedLib
 from seahub.api2.endpoints.admin.user_activities import UserActivitiesView
 from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
 from seahub.api2.endpoints.admin.notifications import AdminNotificationsView
-from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView
+from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView, \
+    AdminSysUserNotificationView, AdminSysUserNotificationsView
 from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \
     AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs, AdminLogGroupMemberAuditLogs
 from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition
@@ -530,6 +532,8 @@ urlpatterns = [
     re_path(r'^api/v2.1/sdoc-notification/$', SdocNotificationView.as_view(), name='api-v2.1-notification'),
     re_path(r'^api/v2.1/all-notifications/$', AllNotificationsView.as_view(), name='api-v2.1-all-notification'),
 
+    re_path(r'^api/v2.1/sys-user-notifications/(?P<nid>\d+)/seen/$', SysUserNotificationSeenView.as_view(), name='api-v2.1-notification-seen'),
+    re_path(r'^api/v2.1/sys-user-notifications/unseen/$', SysUserNotificationUnseenView.as_view(), name='api-v2.1-notification-unseen'),
     ## user::invitations
     re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()),
     re_path(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()),
@@ -796,7 +800,8 @@ urlpatterns = [
     re_path(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'),
     re_path(r'^api/v2.1/admin/sys-notifications/$', AdminSysNotificationsView.as_view(), name='api-2.1-admin-sys-notifications'),
     re_path(r'^api/v2.1/admin/sys-notifications/(?P<nid>\d+)/$', AdminSysNotificationView.as_view(),name='api-2.1-admin-sys-notification'),
-
+    re_path(r'^api/v2.1/admin/sys-user-notifications/$', AdminSysUserNotificationsView.as_view(), name='api-2.1-admin-sys-user-notifications'),
+    re_path(r'^api/v2.1/admin/sys-user-notifications/(?P<nid>\d+)/$', AdminSysUserNotificationView.as_view(), name='api-2.1-admin-sys-user-notification'),
     ## admin::terms and conditions
     re_path(r'^api/v2.1/admin/terms-and-conditions/$', AdminTermsAndConditions.as_view(), name='api-v2.1-admin-terms-and-conditions'),
     re_path(r'^api/v2.1/admin/terms-and-conditions/(?P<term_id>\d+)/$', AdminTermAndCondition.as_view(), name='api-v2.1-admin-term-and-condition'),
diff --git a/seahub/utils/ccnet_db.py b/seahub/utils/ccnet_db.py
index d477607edd..e8ba199b46 100644
--- a/seahub/utils/ccnet_db.py
+++ b/seahub/utils/ccnet_db.py
@@ -244,3 +244,15 @@ class CcnetDB:
             'is_manual_set': is_manual_set
         }
         return CcnetUserRole(**params)
+
+    def get_org_staffs(self, org_id):
+        sql = f"""
+        SELECT email 
+        FROM `{self.db_name}`.`OrgUser`
+        WHERE org_id={org_id} AND is_staff=1
+        """
+        with connection.cursor() as cursor:
+            cursor.execute(sql)
+            staffs = cursor.fetchall()
+
+        return [s[0] for s in staffs]
diff --git a/sql/mysql.sql b/sql/mysql.sql
index 22f6f4dad9..8b96770c45 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -1641,3 +1641,15 @@ CREATE TABLE `group_member_audit` (
   KEY `idx_group_member_audit_user` (`user`),
   KEY `idx_group_member_audit_group_id` (`group_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+CREATE TABLE `notifications_sysusernotification` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `message` longtext NOT NULL,
+  `to_user` varchar(255) NOT NULL,
+  `seen` tinyint(1) NOT NULL,
+  `created_at` datetime(6) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `notifications_sysusernotification_to_user_e0c9101e` (`to_user`),
+  KEY `notifications_sysusernotification_seen_9d851bf7` (`seen`),
+  KEY `notifications_sysusernotification_created_at_56ffd2a0` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;