開箱即用的 GoWind Admin|風(fēng)行,企業(yè)級前后端一體中后臺框架:站內(nèi)信

開箱即用的 GoWind Admin|風(fēng)行,企業(yè)級前后端一體中后臺框架:站內(nèi)信

在企業(yè)級后臺管理系統(tǒng)中,站內(nèi)信是核心溝通組件之一,承擔(dān)著系統(tǒng)通知、用戶互動(dòng)、業(yè)務(wù)提醒等關(guān)鍵場景需求?;?Go 語言微服務(wù)框架 Kratos 構(gòu)建的 Go Wind Admin,將站內(nèi)信模塊封裝為「開箱即用」的標(biāo)準(zhǔn)化組件,無需從零開發(fā)即可快速集成,大幅降低開發(fā)成本。

本文將從功能價(jià)值、技術(shù)設(shè)計(jì)、實(shí)操使用、擴(kuò)展場景四個(gè)維度,全面解析 Go Wind Admin 站內(nèi)信模塊。

一、Go Wind Admin 與站內(nèi)信的核心價(jià)值

1.1 Go Wind Admin 定位

Go Wind Admin 是基于B站 Kratos 微服務(wù)框架開發(fā)的企業(yè)級后臺管理系統(tǒng)解決方案,內(nèi)置用戶管理、權(quán)限控制、日志審計(jì)、配置中心等核心模塊,支持 Go 生態(tài)主流技術(shù)棧(GORM、Redis、ProtoBuf 等),主打「低代碼集成」與「高擴(kuò)展性」,適用于中小團(tuán)隊(duì)快速搭建后臺系統(tǒng)。

1.2 站內(nèi)信功能的核心場景

站內(nèi)信作為系統(tǒng)內(nèi)「非實(shí)時(shí)但可靠」的溝通載體,核心解決以下問題:

  • 系統(tǒng)通知:如賬號狀態(tài)變更(禁用 / 啟用)、權(quán)限調(diào)整、訂單審核結(jié)果等業(yè)務(wù)通知;
  • 用戶互動(dòng):如管理員向指定用戶發(fā)送定向提醒、用戶間基于系統(tǒng)的留言溝通;
  • 消息追溯:所有消息持久化存儲,支持歷史查詢,滿足審計(jì)與問題排查需求;
  • 低干擾觸達(dá):區(qū)別于短信 / 郵件的外部推送,站內(nèi)信僅在系統(tǒng)內(nèi)展示,避免用戶信息過載。

二、站內(nèi)信核心技術(shù)設(shè)計(jì)

Go Wind Admin 站內(nèi)信模塊遵循「簡潔可靠、易于擴(kuò)展」的設(shè)計(jì)原則,核心分為數(shù)據(jù)模型與業(yè)務(wù)邏輯兩層。

2.1 數(shù)據(jù)模型設(shè)計(jì)(Postgresql)

CREATE TABLE public.internal_messages (
    id          bigint generated by default as identity primary key COMMENT 'id',
    created_at  timestamp with time zone COMMENT '創(chuàng)建時(shí)間',
    updated_at  timestamp with time zone COMMENT '更新時(shí)間',
    deleted_at  timestamp with time zone COMMENT '刪除時(shí)間',
    created_by  bigint COMMENT '創(chuàng)建者ID',
    updated_by  bigint COMMENT '更新者ID',
    deleted_by  bigint COMMENT '刪除者ID',
    tenant_id   bigint COMMENT '租戶ID',
    title       varchar COMMENT '消息標(biāo)題',
    content     varchar COMMENT '消息內(nèi)容',
    sender_id   bigint COMMENT '發(fā)送者用戶ID',
    category_id bigint COMMENT '分類ID',
    status      varchar default 'DRAFT'::character varying COMMENT '消息狀態(tài)',
    type        varchar default 'NOTIFICATION'::character varying COMMENT '消息類型'
) COMMENT '站內(nèi)信消息表';

CREATE TABLE public.internal_message_recipients (
    id                bigint generated by default as identity primary key COMMENT 'id',
    created_at        timestamp with time zone COMMENT '創(chuàng)建時(shí)間',
    updated_at        timestamp with time zone COMMENT '更新時(shí)間',
    deleted_at        timestamp with time zone COMMENT '刪除時(shí)間',
    tenant_id         bigint COMMENT '租戶ID',
    message_id        bigint COMMENT '站內(nèi)信內(nèi)容ID',
    recipient_user_id bigint COMMENT '接收者用戶ID',
    status            varchar COMMENT '消息狀態(tài)',
    received_at       timestamp with time zone COMMENT '消息到達(dá)用戶收件箱的時(shí)間',
    read_at           timestamp with time zone COMMENT '用戶閱讀消息的時(shí)間'
) COMMENT '站內(nèi)信消息用戶接收信息表';

CREATE TABLE public.internal_message_categories (
    id         bigint generated by default as identity primary key COMMENT 'id',
    created_at timestamp with time zone        COMMENT '創(chuàng)建時(shí)間',
    updated_at timestamp with time zone        COMMENT '更新時(shí)間',
    deleted_at timestamp with time zone        COMMENT '刪除時(shí)間',
    created_by bigint        COMMENT '創(chuàng)建者ID',
    updated_by bigint        COMMENT '更新者ID',
    deleted_by bigint        COMMENT '刪除者ID',
    is_enabled boolean default true        COMMENT '是否啟用',
    sort_order integer default 0        COMMENT '排序順序,值越小越靠前',
    remark     varchar        COMMENT '備注',
    tenant_id  bigint        COMMENT '租戶ID',
    name       varchar        COMMENT '名稱',
    code       varchar        COMMENT '編碼',
    icon_url   varchar        COMMENT '圖標(biāo)URL',
    parent_id  bigint
        constraint internal_message_categories_in_8a268228b9922ecb0c6e7d2099d6aa98
            references public.internal_message_categories
            on delete set null
        COMMENT '父節(jié)點(diǎn)ID'
) COMMENT '站內(nèi)信消息分類表';

目前,站內(nèi)信功能只設(shè)計(jì)了三張表,用于系統(tǒng)通知。

在 Go Wind Admin 中,數(shù)據(jù)模型已通過 Ent的Schema 進(jìn)行了定義,開發(fā)者可直接調(diào)用:

// app/admin/service/internal/data/ent/schema/internal_message.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema"
    "entgo.io/ent/schema/field"
    "github.com/tx7do/go-utils/entgo/mixin"
)

// InternalMessage holds the schema definition for the InternalMessage entity.
type InternalMessage struct {
    ent.Schema
}

func (InternalMessage) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{
            Table:     "internal_messages",
            Charset:   "utf8mb4",
            Collation: "utf8mb4_bin",
        },
        entsql.WithComments(true),
        schema.Comment("站內(nèi)信消息表"),
    }
}

// Fields of the InternalMessage.
func (InternalMessage) Fields() []ent.Field {
    return []ent.Field{
        field.String("title").
            Comment("消息標(biāo)題").
            Optional().
            Nillable(),

        field.String("content").
            Comment("消息內(nèi)容").
            Optional().
            Nillable(),

        field.Uint32("sender_id").
            Comment("發(fā)送者用戶ID").
            Optional().
            Nillable(),

        field.Uint32("category_id").
            Comment("分類ID").
            Optional().
            Nillable(),

        field.Enum("status").
            Comment("消息狀態(tài)").
            NamedValues(
                "Draft", "DRAFT",
                "Published", "PUBLISHED",
                "Scheduled", "SCHEDULED",
                "Revoked", "REVOKED",
                "Archived", "ARCHIVED",
                "Deleted", "DELETED",
            ).
            Default("DRAFT").
            Optional().
            Nillable(),

        field.Enum("type").
            Comment("消息類型").
            NamedValues(
                "Notification", "NOTIFICATION",
                "Private", "PRIVATE",
                "Group", "GROUP",
            ).
            Default("NOTIFICATION").
            Optional().
            Nillable(),
    }
}

// Mixin of the InternalMessage.
func (InternalMessage) Mixin() []ent.Mixin {
    return []ent.Mixin{
        mixin.AutoIncrementId{},
        mixin.TimeAt{},
        mixin.OperatorID{},
        mixin.TenantID{},
    }
}

2.2 核心業(yè)務(wù)邏輯

Go Wind Admin 已封裝站內(nèi)信全生命周期邏輯,核心流程如下:

  1. 參數(shù)校驗(yàn)
  2. 數(shù)據(jù)組裝
  3. 將站內(nèi)信消息存入數(shù)據(jù)庫;
  4. 將站內(nèi)信消息分發(fā)給用戶的收件箱;
  5. 通過SSE通知前端。

核心代碼片段(發(fā)送邏輯):

// app/admin/service/internal/service/internal_message_service.go

// SendMessage 發(fā)送消息
func (s *InternalMessageService) SendMessage(ctx context.Context, req *internalMessageV1.SendMessageRequest) (*internalMessageV1.SendMessageResponse, error) {
    // 獲取操作人信息
    operator, err := auth.FromContext(ctx)
    if err != nil {
        return nil, err
    }

    now := time.Now()

    var msg *internalMessageV1.InternalMessage
    if msg, err = s.internalMessageRepo.Create(ctx, &internalMessageV1.CreateInternalMessageRequest{
        Data: &internalMessageV1.InternalMessage{
            Title:      req.Title,
            Content:    trans.Ptr(req.GetContent()),
            Status:     trans.Ptr(internalMessageV1.InternalMessage_PUBLISHED),
            Type:       trans.Ptr(req.GetType()),
            CategoryId: req.CategoryId,
            CreatedBy:  trans.Ptr(operator.GetUserId()),
            CreatedAt:  timeutil.TimeToTimestamppb(&now),
        },
    }); err != nil {
        s.log.Errorf("create internal message failed: %s", err)
        return nil, err
    }

    if req.GetTargetAll() {
        users, err := s.userRepo.List(ctx, &pagination.PagingRequest{NoPaging: trans.Ptr(true)})
        if err != nil {
            s.log.Errorf("send message failed, list users failed, %s", err)
        } else {
            for _, user := range users.Items {
                _ = s.sendNotification(ctx, msg.GetId(), user.GetId(), operator.GetUserId(), &now, msg.GetTitle(), msg.GetContent())
            }
        }
    } else {
        if req.RecipientUserId != nil {
            _ = s.sendNotification(ctx, msg.GetId(), req.GetRecipientUserId(), operator.GetUserId(), &now, msg.GetTitle(), msg.GetContent())
        } else {
            if len(req.TargetUserIds) != 0 {
                for _, uid := range req.TargetUserIds {
                    _ = s.sendNotification(ctx, msg.GetId(), uid, operator.GetUserId(), &now, msg.GetTitle(), msg.GetContent())
                }
            }
        }
    }

    return &internalMessageV1.SendMessageResponse{
        MessageId: msg.GetId(),
    }, nil
}

// sendNotification 向客戶端發(fā)送通知消息
func (s *InternalMessageService) sendNotification(ctx context.Context, messageId uint32, recipientUserId uint32, senderUserId uint32, now *time.Time, title, content string) error {
    recipient := &internalMessageV1.InternalMessageRecipient{
        MessageId:       trans.Ptr(messageId),
        RecipientUserId: trans.Ptr(recipientUserId),
        Status:          trans.Ptr(internalMessageV1.InternalMessageRecipient_SENT),
        CreatedBy:       trans.Ptr(senderUserId),
        CreatedAt:       timeutil.TimeToTimestamppb(now),
        Title:           trans.Ptr(title),
        Content:         trans.Ptr(content),
    }

    var err error
    var entity *internalMessageV1.InternalMessageRecipient
    if entity, err = s.internalMessageRecipientRepo.Create(ctx, recipient); err != nil {
        s.log.Errorf("send message failed, send to user failed, %s", err)
        return err
    }
    recipient.Id = entity.Id

    recipientJson, _ := json.Marshal(recipient)

    recipientStreamIds := s.userToken.GetAccessToken(ctx, recipientUserId)
    for _, streamId := range recipientStreamIds {
        s.sseServer.Publish(ctx, sse.StreamID(streamId), &sse.Event{
            ID:    []byte(uuid.New().String()),
            Data:  recipientJson,
            Event: []byte("notification"),
        })
    }

    return nil
}

三、API 接口設(shè)計(jì)與使用

Go Wind Admin 站內(nèi)信模塊提供 RESTful 風(fēng)格 API,基于 ProtoBuf 定義接口規(guī)范,支持跨語言調(diào)用。

3.1 核心 API 列表(Proto 定義)

// api/protos/admin/service/v1/i_internal_message.proto
syntax = "proto3";

package admin.service.v1;

import "gnostic/openapi/v3/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";

import "pagination/v1/pagination.proto";

import "internal_message/service/v1/internal_message.proto";

// 站內(nèi)信消息管理服務(wù)
service InternalMessageService {
  // 查詢站內(nèi)信消息列表
  rpc ListMessage(pagination.PagingRequest) returns (internal_message.service.v1.ListInternalMessageResponse) {
    option (google.api.http) = {
      get: "/admin/v1/internal-message/messages"
    };
  }

  // 查詢站內(nèi)信消息詳情
  rpc GetMessage(internal_message.service.v1.GetInternalMessageRequest) returns (internal_message.service.v1.InternalMessage) {
    option (google.api.http) = {
      get: "/admin/v1/internal-message/messages/{id}"
    };
  }

  // 更新站內(nèi)信消息
  rpc UpdateMessage(internal_message.service.v1.UpdateInternalMessageRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      put: "/admin/v1/internal-message/messages/{data.id}"
      body: "*"
    };
  }

  // 刪除站內(nèi)信消息
  rpc DeleteMessage(internal_message.service.v1.DeleteInternalMessageRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      delete: "/admin/v1/internal-message/messages/{id}"
    };
  }


  // 發(fā)送消息
  rpc SendMessage(internal_message.service.v1.SendMessageRequest) returns (internal_message.service.v1.SendMessageResponse) {
    option (google.api.http) = {
      post: "/admin/v1/internal-message/send"
      body: "*"
    };
  }

  // 獲取用戶的收件箱列表 (通知類)
  rpc ListUserInbox(pagination.PagingRequest) returns (internal_message.service.v1.ListUserInboxResponse) {
    option (google.api.http) = {
      get: "/admin/v1/internal-message/inbox"
    };
  }

  // 刪除用戶收件箱中的通知記錄
  rpc DeleteNotificationFromInbox(internal_message.service.v1.DeleteNotificationFromInboxRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      post: "/admin/v1/internal-message/inbox/delete"
      body: "*"
    };
  }

  // 將通知標(biāo)記為已讀
  rpc MarkNotificationAsRead(internal_message.service.v1.MarkNotificationAsReadRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      post: "/admin/v1/internal-message/read"
      body: "*"
    };
  }

  // 撤銷某條消息
  rpc RevokeMessage(internal_message.service.v1.RevokeMessageRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      post: "/admin/v1/internal-message/revoke"
      body: "*"
    };
  }
}

3.2 API 調(diào)用示例(curl)

(1)發(fā)送系統(tǒng)通知

curl -X POST http://127.0.0.1:8000/api/v1/internal-message/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {admin_token}" \
-d '{
    "type": "NOTIFICATION",
    "recipientUserId": 0,
    "conversationId": 0,
    "categoryId": 0,
    "targetAll": true,
    "title": "賬號權(quán)限更新",
    "content": "您的賬號已添加「訂單審核」權(quán)限,生效時(shí)間:2024-10-01",
}'

響應(yīng)結(jié)果:

{
  "messageId": 0
}

四、前端對接

在站內(nèi)信功能中,「實(shí)時(shí)性」是提升用戶體驗(yàn)的關(guān)鍵 —— 用戶無需刷新頁面,就能即時(shí)收到新消息提醒。這段代碼基于 SSE(Server-Sent Events,服務(wù)器發(fā)送事件) 實(shí)現(xiàn)前端實(shí)時(shí)通知接收,適配 Go Wind Admin 后端的推送能力。

// apps/admin/src/layouts/basic.vue

function handleSseNotification(
  data: InternalMessageRecipient,
  event: MessageEvent,
) {
  console.log('SSE', event, data);

  if (!hasMessage(data)) {
    notifications.value.unshift(convertInternalMessageRecipient(data));
  }
}

function initSseClient() {
  const targetSseUrl = `${import.meta.env.VITE_GLOB_SSE_URL}?stream=${encodeURIComponent(accessStore.accessToken)}`;
  const sseClient = new SSEClient({
    url: targetSseUrl,
    withCredentials: false,
  });

  sseClient.connect();
  sseClient.on<InternalMessageRecipient>('notification', handleSseNotification);
}

五、總結(jié)與展望

Go Wind Admin 站內(nèi)信模塊通過「標(biāo)準(zhǔn)化數(shù)據(jù)模型 + 封裝核心邏輯 + 開放 API 接口」,實(shí)現(xiàn)了「開箱即用」的特性,開發(fā)者無需關(guān)注底層存儲與流程設(shè)計(jì),僅需通過 API 即可快速集成。目前模塊已支持消息發(fā)送、讀取、過期清理等基礎(chǔ)功能,未來將進(jìn)一步優(yōu)化:

  • 新增消息撤回功能(支持發(fā)送后 N 分鐘內(nèi)撤回);
  • 支持消息標(biāo)簽(如「重要」「工作」),提升篩選效率;
  • 集成消息搜索(基于 Elasticsearch),支持全文檢索。

若你在使用過程中遇到問題,可通過 Go Wind Admin 官方 GitHub 提交 Issue,或參與社區(qū)討論獲取支持。

項(xiàng)目代碼

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容