GoWindGoWind
Home
Framework
GoWind Toolkit
  • Open Source

    • GoWind Admin (Admin Platform)
    • GoWind CMS (Content Management)
    • GoWind UBA (Behavior Analytics)
    • GoWind Shop (E-commerce)
    • GoWind Kanban (Kanban Boards)
    • GoWind Ledger (Personal Finance)
    • GoWind ShortPlay (Short Drama)
    • GoWind Ops (Operations Platform)
    • RushWind (Rust Ecosystem)
  • Open Core (Community + Enterprise)

    • GoWind OA (Office Collaboration)
    • GoWind ERP (Procure-to-Pay)
    • GoWind WMS (Warehousing)
  • Commercial

    • GoWind IM (Instant Messaging)
    • GoWind IoT (IoT Platform)
    • GoWind Edu (AI Education)
    • GoWind MES (Manufacturing Execution)
    • GoWind CDC (Data Pipeline)
    • GoWind SDP (Zero-Trust Gateway)
    • GoWind Quant (Quant Research)
    • GoWind Trade Radar (Export Sourcing)
    • GoWind Game Radar (Game Greenlight)
  • 简体中文
  • English
GitHub
Home
Framework
GoWind Toolkit
  • Open Source

    • GoWind Admin (Admin Platform)
    • GoWind CMS (Content Management)
    • GoWind UBA (Behavior Analytics)
    • GoWind Shop (E-commerce)
    • GoWind Kanban (Kanban Boards)
    • GoWind Ledger (Personal Finance)
    • GoWind ShortPlay (Short Drama)
    • GoWind Ops (Operations Platform)
    • RushWind (Rust Ecosystem)
  • Open Core (Community + Enterprise)

    • GoWind OA (Office Collaboration)
    • GoWind ERP (Procure-to-Pay)
    • GoWind WMS (Warehousing)
  • Commercial

    • GoWind IM (Instant Messaging)
    • GoWind IoT (IoT Platform)
    • GoWind Edu (AI Education)
    • GoWind MES (Manufacturing Execution)
    • GoWind CDC (Data Pipeline)
    • GoWind SDP (Zero-Trust Gateway)
    • GoWind Quant (Quant Research)
    • GoWind Trade Radar (Export Sourcing)
    • GoWind Game Radar (Game Greenlight)
  • 简体中文
  • English
GitHub
  • Introduction

    • GoWind CMS Product Introduction
    • GoWind CMS Installation Guide
  • Backend

    • CMS Backend Architecture Overview
    • CMS Backend Modules Overview
    • CMS Protobuf API Definitions
    • CMS Configuration and Deployment Guide
    • Backend Extension Mechanisms Guide
  • Frontend

    • CMS Frontend Architecture
    • CMS Frontend Modules Overview
  • Getting Started

    • Adding a Content Type, Full Stack in Practice
    • Multi-Client API Client Code Generation Tutorial
  • Core Tutorials

    • Content Multilingual Translation Tutorial
    • Content Publishing Workflow Tutorial
    • Section Editor in Practice
    • Headless API Multi-Client Integration in Practice
    • Frontend Application Development in Practice
  • Intermediate Tutorials

    • Multi-Site Management in Practice
    • Media Asset Management in Practice
    • Comment System Tutorial
    • Event Bus Architecture in Practice
    • Lua Script Extensions in Practice
  • Advanced Topics

    • Permission System in Practice
    • Dual-Client Login Security in Practice
    • Encryption and Security Toolkit Tutorial
    • Full-Text Search in Practice
    • Task Scheduling in Practice
    • Real-Time Message Push (SSE) in Practice
    • Performance Monitoring in Practice
    • Dictionary Management System Tutorial
  • Full-Stack Guides

    • Full-Stack Integration in Practice
    • Three-Service Deployment Tutorial

Real-Time Message Push (SSE) in Practice

GoWind CMS integrates SSE (Server-Sent Events) services in both the Admin Service and the App Service to push real-time messages to the admin console and frontend applications. This tutorial explains the SSE architecture design, push scenarios, and the front-/back-end implementation.

Prerequisites

  • You have read CMS Backend Architecture Overview
  • Understand the concept of HTTP long-lived connections
  • It is recommended to first read the GoWind Admin SSE Push Tutorial

1. SSE Architecture

1.1 The CMS Dual SSE Services

SSE ServicePortAudiencePushed Content
Admin SSE6601AdministratorsComment moderation reminders, system alerts, task status
App SSE6701Frontend usersComment replies, interaction notifications, system messages

1.2 SSE vs. WebSocket

ComparisonSSEWebSocket
Communication directionServer → client (one-way)Bidirectional
ProtocolHTTPWebSocket
ReconnectionAutomatic browser reconnectionManual implementation required
ComplexityLowHigh
Use casesNotification push, status updatesChat, real-time collaboration

CMS notification scenarios only require server-side push, so SSE is the simpler choice.

2. Server-Side Implementation

2.1 The SSE Server

// app/admin/service/internal/server/sse_server.go
type SSEServer struct {
    clients map[uint32]*client.Player  // userId → 客户端连接
    mu      sync.RWMutex
}

func NewSSEServer() *SSEServer {
    return &SSEServer{
        clients: make(map[uint32]*client.Player),
    }
}

// SSE 端点,前端 EventSource 连接
func (s *SSEServer) HandleSSE(c *gin.Context) {
    userId := c.GetUint32("userId")

    c.Header("Content-Type", "text/event-stream")
    c.Header("Cache-Control", "no-cache")
    c.Header("Connection", "keep-alive")
    c.Header("Access-Control-Allow-Origin", "*")

    // 注册客户端连接
    player := client.NewPlayer(userId, c.Writer)
    s.register(userId, player)
    defer s.unregister(userId)

    // 保持连接
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-c.Done():
            return
        case <-ticker.C:
            // 心跳保活
            c.SSEvent("ping", "heartbeat")
            c.Writer.Flush()
        }
    }
}

// 向指定用户推送消息
func (s *SSEServer) Push(userId uint32, event string, data interface{}) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    if client, exists := s.clients[userId]; exists {
        client.Send(event, data)
    }
}

// 广播给所有在线用户
func (s *SSEServer) Broadcast(event string, data interface{}) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    for _, client := range s.clients {
        client.Send(event, data)
    }
}

2.2 Event Bus Integration

Events in the Core Service trigger SSE push (SSE) through the EventBus:

// app/core/service/internal/service/event_handler.go
func (s *EventHandler) OnCommentCreated(ctx context.Context, event *CommentCreatedEvent) {
    // 1. 通知管理后台:新评论待审核
    s.adminSSEClient.Push(event.PostAuthorId, "comment:pending", map[string]any{
        "commentId": event.CommentId,
        "postId":    event.PostId,
        "content":   event.Content,
        "userName":  event.UserName,
    })

    // 2. 通知前台用户:文章有新评论
    s.appSSEClient.Push(event.PostAuthorId, "comment:new", map[string]any{
        "postId":    event.PostId,
        "postTitle": event.PostTitle,
        "userName":  event.UserName,
    })
}

func (s *EventHandler) OnCommentReplied(ctx context.Context, event *CommentRepliedEvent) {
    // 通知被回复的用户
    s.appSSEClient.Push(event.ReplyToUserId, "comment:reply", map[string]any{
        "commentId":   event.CommentId,
        "postId":      event.PostId,
        "replyByUser": event.UserName,
        "content":     event.Content,
    })
}

2.3 Registering Event Subscriptions

// app/core/service/internal/service/service.go
func InitEventHandlers(eventbus *eventbus.EventBus, adminSSE *sse.Client, appSSE *sse.Client) {
    handler := &EventHandler{adminSSEClient: adminSSE, appSSEClient: appSSE}

    // 订阅评论事件
    eventbus.Subscribe("comment.created", handler.OnCommentCreated)
    eventbus.Subscribe("comment.replied", handler.OnCommentReplied)
    eventbus.Subscribe("comment.approved", handler.OnCommentApproved)

    // 订阅帖子事件
    eventbus.Subscribe("post.published", handler.OnPostPublished)
    eventbus.Subscribe("post.updated", handler.OnPostUpdated)
}

3. Admin Console Frontend Implementation

3.1 SSE Connection Management

// frontend/admin/apps/admin/src/utils/sse.ts
class SSEClient {
  private eventSource: EventSource | null = null;
  private listeners: Map<string, Set<(data: any) => void>> = new Map();
  private reconnectTimer: NodeJS.Timeout | null = null;

  connect(token: string) {
    if (this.eventSource) {
      this.eventSource.close();
    }

    this.eventSource = new EventSource(
      `${import.meta.env.VITE_SSE_URL}/events?token=${token}`,
    );

    this.eventSource.onopen = () => {
      console.log('SSE 已连接');
    };

    this.eventSource.onerror = () => {
      console.log('SSE 连接断开,5 秒后重连...');
      this.eventSource?.close();
      this.reconnectTimer = setTimeout(() => this.connect(token), 5000);
    };

    // 注册事件监听
    this.listeners.forEach((callbacks, eventName) => {
      this.eventSource?.addEventListener(eventName, (e: MessageEvent) => {
        const data = JSON.parse(e.data);
        callbacks.forEach((cb) => cb(data));
      });
    });
  }

  on(event: string, callback: (data: any) => void) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
  }

  off(event: string, callback: (data: any) => void) {
    this.listeners.get(event)?.delete(callback);
  }

  disconnect() {
    this.eventSource?.close();
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
  }
}

export const sseClient = new SSEClient();

3.2 Global Notification Integration

// frontend/admin/apps/admin/src/App.vue
import { sseClient } from '@/utils/sse';
import { useNotification } from 'ant-design-vue';

const { info, success, warning } = useNotification();
const token = getToken();

// 连接 SSE
sseClient.connect(token);

// 新评论审核提醒
sseClient.on('comment:pending', (data) => {
  warning({
    title: '新评论待审核',
    content: `${data.userName}: ${data.content}`,
    onClick: () => router.push('/content/comment/audit'),
  });
});

// 系统告警
sseClient.on('system:alert', (data) => {
  info({
    title: '系统告警',
    content: data.message,
  });
});

onUnmounted(() => {
  sseClient.disconnect();
});

3.3 Badge Notifications

<!-- components/layout/Header.vue -->
<script setup lang="ts">
import { sseClient } from '@/utils/sse';

const pendingCount = ref(0);

onMounted(() => {
  sseClient.on('comment:pending', () => {
    pendingCount.value++;
  });
});

const handleClick = () => {
  router.push('/content/comment/audit');
  pendingCount.value = 0;
};
</script>

<template>
  <Badge :count="pendingCount">
    <BellOutlined @click="handleClick" />
  </Badge>
</template>

4. Frontend Application SSE

4.1 React Frontend SSE

// src/lib/sse/client.ts
export class AppSSEClient {
  private eventSource: EventSource | null = null;

  connect(token: string) {
    this.eventSource = new EventSource(
      `${process.env.NEXT_PUBLIC_SSE_URL}/events?token=${token}`,
    );

    this.eventSource.addEventListener('comment:reply', this.onCommentReply);
    this.eventSource.addEventListener('notification', this.onNotification);

    this.eventSource.onerror = () => {
      this.eventSource?.close();
      setTimeout(() => this.connect(token), 5000);
    };
  }

  private onCommentReply = (e: MessageEvent) => {
    const data = JSON.parse(e.data);
    // 显示 toast 通知
    toast.success(`${data.replyByUser} 回复了你的评论`);
  };

  private onNotification = (e: MessageEvent) => {
    const data = JSON.parse(e.data);
    // 更新通知角标
    notificationStore.increment();
  };

  disconnect() {
    this.eventSource?.close();
  }
}

4.2 Notification Center Component

// src/components/layout/NotificationBell.tsx
'use client';

import { useEffect, useState } from 'react';
import { Popover, Badge, List } from 'antd';
import { AppSSEClient } from '@/lib/sse/client';

interface Notification {
  id: number;
  title: string;
  content: string;
  type: string;
  createdAt: string;
}

export function NotificationBell({ token }: { token: string }) {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [unreadCount, setUnreadCount] = useState(0);

  useEffect(() => {
    if (!token) return;

    const sse = new AppSSEClient();
    sse.connect(token);

    return () => sse.disconnect();
  }, [token]);

  return (
    <Popover
      content={
        <List
          dataSource={notifications}
          renderItem={(n) => (
            <List.Item>
              <List.Item.Meta
                title={n.title}
                description={n.content}
              />
            </List.Item>
          )}
        />
      }
      trigger="click"
    >
      <Badge count={unreadCount}>
        <BellIcon />
      </Badge>
    </Popover>
  );
}

4.3 Flutter Frontend SSE

// lib/core/network/sse_client.dart
class SSEClient {
  final String baseUrl;
  late final EventSource _eventSource;

  final _commentReplyController = StreamController<Map<String, dynamic>>.broadcast();
  final _notificationController = StreamController<Map<String, dynamic>>.broadcast();

  Stream<Map<String, dynamic>> get onCommentReply => _commentReplyController.stream;
  Stream<Map<String, dynamic>> get onNotification => _notificationController.stream;

  SSEClient({required this.baseUrl});

  Future<void> connect(String token) async {
    _eventSource = await EventSource.connect(
      '$baseUrl/events?token=$token',
    );

    _eventSource.addEventListener('comment:reply', (event) {
      final data = jsonDecode(event.data);
      _commentReplyController.add(data);
    });

    _eventSource.addEventListener('notification', (event) {
      final data = jsonDecode(event.data);
      _notificationController.add(data);
    });
  }

  void dispose() {
    _eventSource.close();
    _commentReplyController.close();
    _notificationController.close();
  }
}
// 使用示例:在 BLoC 中监听 SSE
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
  final SSEClient sseClient;

  NotificationBloc({required this.sseClient}) : super(NotificationInitial()) {
    sseClient.onCommentReply.listen((data) {
      add(ReceiveNotification(
        title: '${data['replyByUser']} 回复了你',
        content: data['content'],
      ));
    });
  }
}

5. Push Scenarios

5.1 CMS Push Scenario Overview

5.2 Custom Push

Push logic can be customized through Lua scripts:

-- 文章发布时推送通知给所有关注者
eventbus.subscribe("post.published", function(event)
    local post_id = event.post_id
    local author_id = event.author_id

    -- 查询所有关注者
    local followers = api.get("/admin/v1/users/" .. author_id .. "/followers")

    for _, follower_id in ipairs(followers) do
        -- 推送 SSE 通知
        sse.push_to_app_user(follower_id, "post:new", {
            postId = post_id,
            authorName = event.author_name,
            title = event.title,
        })
    end
end)

6. Performance and Reliability

6.1 Connection Management

MetricRecommendation
Maximum connections5000-10000 per instance
Heartbeat interval30 seconds
Timeout disconnectNo response for 60 seconds
Horizontal scalingNginx load balancing + Redis Pub/Sub

6.2 Horizontal Scaling

When deploying multiple instances, pushes are synchronized through Redis Pub/Sub:

// Redis Pub/Sub 广播
func (s *SSEServer) Push(userId uint32, event string, data interface{}) {
    msg := PushMessage{
        UserId: userId,
        Event:  event,
        Data:   data,
    }
    payload, _ := json.Marshal(msg)
    // 发布到 Redis,所有 SSE 实例订阅
    redisClient.Publish(ctx, "sse:push", payload)
}

// 每个 SSE 实例订阅 Redis
func (s *SSEServer) SubscribeRedis() {
    sub := redisClient.Subscribe(ctx, "sse:push")
    for msg := range sub.Channel() {
        var pushMsg PushMessage
        json.Unmarshal([]byte(msg.Payload), &pushMsg)

        // 只推送给本实例连接的用户
        s.localPush(pushMsg.UserId, pushMsg.Event, pushMsg.Data)
    }
}

7. Checklist

Check ItemDescription
SSE serverDual services: Admin SSE + App SSE
Event subscriptionEventBus integration
Admin console frontendSSE connection + notification display
Frontend clientReact/Flutter SSE clients
Heartbeat keep-alive30-second heartbeat
Automatic reconnectionReconnect 5 seconds after disconnection
Horizontal scalingRedis Pub/Sub broadcast
Message typesComments/notifications/system alerts

Related Documents

  • CMS Backend Architecture Overview
  • Comment System in Practice
  • Task Scheduling in Practice
  • GoWind Admin SSE Push Tutorial
Edit this page
Last Updated:: 11/3/26, 2:14 PM
Contributors: Bobo
Prev
Task Scheduling in Practice
Next
Performance Monitoring in Practice