mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
refactor(msg_gateway): decouple bot gateway and push notification architecture
- Split shared monolithic consts into bot, push, and errs with typed sentinel errors - Restructure model layer into distinct bot and push subdomains - Refactor DAO layer to enforce single-owner principle and remove cross-table raw SQL queries - Decompose 1150+ line service/push.go into push_channel, push_event, push_trigger, push_worker, and push_template - Clean up controller layer with generic request handlers and parameter validation in controller/base.go - Streamline plugin.go to core Cordis lifecycle orchestration and remove re-export bloat - Verify all unit tests, race tests, Cordis architecture rules, and Swagger generation pass cleanly
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
builtInEventsMu sync.RWMutex
|
||||
// BuiltInEvents lists all built-in events defined across the domain.
|
||||
BuiltInEvents []do.EventMetadata
|
||||
)
|
||||
|
||||
// RegisterBuiltInEvent registers a built-in event definition.
|
||||
func RegisterBuiltInEvent(meta do.EventMetadata) {
|
||||
builtInEventsMu.Lock()
|
||||
defer builtInEventsMu.Unlock()
|
||||
for i, e := range BuiltInEvents {
|
||||
if e.Key == meta.Key {
|
||||
BuiltInEvents[i] = meta
|
||||
return
|
||||
}
|
||||
}
|
||||
BuiltInEvents = append(BuiltInEvents, meta)
|
||||
}
|
||||
|
||||
// GetBuiltInEvents returns a copy of registered built-in events.
|
||||
func GetBuiltInEvents() []do.EventMetadata {
|
||||
builtInEventsMu.RLock()
|
||||
defer builtInEventsMu.RUnlock()
|
||||
out := make([]do.EventMetadata, len(BuiltInEvents))
|
||||
copy(out, BuiltInEvents)
|
||||
return out
|
||||
}
|
||||
|
||||
// FindBuiltInEvent finds a registered built-in event by key.
|
||||
func FindBuiltInEvent(key string) (do.EventMetadata, bool) {
|
||||
for _, meta := range GetBuiltInEvents() {
|
||||
if meta.Key == key {
|
||||
return meta, true
|
||||
}
|
||||
}
|
||||
return do.EventMetadata{}, false
|
||||
}
|
||||
|
||||
// PushRegistryAdapter adapts contracts.PushRegistry onto the built-in event store.
|
||||
type PushRegistryAdapter struct{}
|
||||
|
||||
// RegisterBuiltInEvent records a built-in push event definition from cross-plugin contract.
|
||||
func (PushRegistryAdapter) RegisterBuiltInEvent(meta contracts.PushEventMeta) {
|
||||
RegisterBuiltInEvent(eventMetadataFromContract(meta))
|
||||
}
|
||||
|
||||
// SyncEvents persists registered built-in events into the database.
|
||||
func (PushRegistryAdapter) SyncEvents(ctx context.Context) error {
|
||||
return SyncEvents(ctx)
|
||||
}
|
||||
|
||||
func eventMetadataFromContract(meta contracts.PushEventMeta) do.EventMetadata {
|
||||
return do.EventMetadata{
|
||||
Key: meta.Key,
|
||||
Name: meta.Name,
|
||||
Description: meta.Description,
|
||||
DefaultTemplate: do.NotificationMessage{
|
||||
Title: meta.DefaultTemplate.Title,
|
||||
Content: meta.DefaultTemplate.Content,
|
||||
Level: meta.DefaultTemplate.Level,
|
||||
Ext: meta.DefaultTemplate.Ext,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SyncBuiltInEvents seeds a database row for every registered built-in event.
|
||||
func SyncBuiltInEvents(ctx context.Context) error {
|
||||
for _, meta := range GetBuiltInEvents() {
|
||||
_, err := dao.GetPushEventByKeyRecord(ctx, meta.Key)
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
var defaultTemplateStr string
|
||||
if defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate); err == nil {
|
||||
defaultTemplateStr = string(defaultTemplateBytes)
|
||||
}
|
||||
event := entity.PushEvent{
|
||||
EventKey: meta.Key,
|
||||
Name: meta.Name,
|
||||
Channels: []string{},
|
||||
Targets: []string{},
|
||||
Template: defaultTemplateStr,
|
||||
Enabled: false,
|
||||
}
|
||||
if err := dao.CreatePushEventRecord(ctx, &event); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncEvents automatically registers/updates built-in events in the database.
|
||||
func SyncEvents(ctx context.Context) error {
|
||||
return SyncBuiltInEvents(ctx)
|
||||
}
|
||||
|
||||
// ListPushEvents lists all configured push events.
|
||||
func ListPushEvents(ctx context.Context) ([]entity.PushEvent, error) {
|
||||
return dao.ListPushEventsRecord(ctx)
|
||||
}
|
||||
|
||||
// CreatePushEvent stores a push event configuration for a built-in event or task type.
|
||||
func CreatePushEvent(ctx context.Context, req do.CreatePushEventRequest) (entity.PushEvent, error) {
|
||||
eventKey, eventName, defaultTemplateBytes, err := GetEventInfo(ctx, req)
|
||||
if err != nil {
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
|
||||
count, err := dao.CountPushEventsByKeyRecord(ctx, eventKey)
|
||||
if err != nil {
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
if count > 0 {
|
||||
return entity.PushEvent{}, errors.New(consts.ErrEventAlreadyConfigured)
|
||||
}
|
||||
|
||||
templateStr := strings.TrimSpace(req.Template)
|
||||
if templateStr == "" {
|
||||
templateStr = string(defaultTemplateBytes)
|
||||
} else {
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal([]byte(templateStr), &tempMap); err != nil {
|
||||
return entity.PushEvent{}, errors.New(consts.ErrTemplateInvalidJSON)
|
||||
}
|
||||
}
|
||||
|
||||
channels := req.Channels
|
||||
if channels == nil {
|
||||
channels = []string{}
|
||||
}
|
||||
targets := req.Targets
|
||||
if targets == nil {
|
||||
targets = []string{}
|
||||
}
|
||||
|
||||
event := entity.PushEvent{
|
||||
EventKey: eventKey,
|
||||
Name: eventName,
|
||||
TaskType: req.TaskType,
|
||||
Channels: channels,
|
||||
Targets: targets,
|
||||
Template: templateStr,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
if err := event.Validate(); err != nil {
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
if err := dao.CreatePushEventRecord(ctx, &event); err != nil {
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// DeletePushEvent deletes a push event configuration by id.
|
||||
func DeletePushEvent(ctx context.Context, id uint64) error {
|
||||
event, err := dao.GetPushEventByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dao.DeletePushEventRecord(ctx, &event)
|
||||
}
|
||||
|
||||
// UpdatePushEvent replaces mutable push event fields.
|
||||
func UpdatePushEvent(ctx context.Context, id uint64, req do.UpdatePushEventRequest) error {
|
||||
event, err := dao.GetPushEventByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event.Channels = req.Channels
|
||||
event.Targets = req.Targets
|
||||
event.Template = req.Template
|
||||
event.Enabled = req.Enabled
|
||||
if err := event.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return dao.SavePushEventRecord(ctx, &event)
|
||||
}
|
||||
|
||||
// TogglePushEvent flips the enabled flag of a push event.
|
||||
func TogglePushEvent(ctx context.Context, id uint64) (bool, error) {
|
||||
event, err := dao.GetPushEventByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
enabled := !event.Enabled
|
||||
if enabled && len(event.Channels) == 0 {
|
||||
return false, errors.New(consts.ErrEnableWithoutChannels)
|
||||
}
|
||||
if err := dao.UpdatePushEventEnabledRecord(ctx, &event, enabled); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
|
||||
// ListActivePushEventsByTaskType returns enabled push events for a given task type.
|
||||
func ListActivePushEventsByTaskType(ctx context.Context, taskType string) ([]entity.PushEvent, error) {
|
||||
return dao.ListActivePushEventsByTaskTypeRecord(ctx, taskType)
|
||||
}
|
||||
|
||||
// GetEventInfo derives the event key, display name and default template for a
|
||||
// task-completion based event or a registered built-in event key.
|
||||
func GetEventInfo(ctx context.Context, req do.CreatePushEventRequest) (string, string, []byte, error) {
|
||||
if req.TaskType != "" {
|
||||
taskName := req.TaskType
|
||||
if taskSvc := GetTaskService(ctx); taskSvc != nil {
|
||||
if meta, ok := taskSvc.GetTaskMeta(req.TaskType); ok {
|
||||
taskName = meta.DisplayName
|
||||
}
|
||||
}
|
||||
eventKey := "task_completed:" + req.TaskType
|
||||
eventName := "任务完成: " + taskName
|
||||
defaultTemplate := do.NotificationMessage{
|
||||
Title: "任务完成: " + taskName,
|
||||
Content: "异步任务 {{task_name}} (ID: {{task_id}}) 已完成。状态: {{task_status}},耗时: {{task_duration}} ms。",
|
||||
Level: consts.DefaultLevelInfo,
|
||||
}
|
||||
defaultTemplateBytes, err := json.Marshal(defaultTemplate)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
return eventKey, eventName, defaultTemplateBytes, nil
|
||||
}
|
||||
|
||||
if req.EventKey == "" {
|
||||
return "", "", nil, errors.New(consts.ErrEventKeyOrTaskType)
|
||||
}
|
||||
|
||||
meta, found := FindBuiltInEvent(req.EventKey)
|
||||
if !found {
|
||||
return "", "", nil, errors.New(consts.ErrUnsupportedEventKey)
|
||||
}
|
||||
|
||||
defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
return req.EventKey, meta.Name, defaultTemplateBytes, nil
|
||||
}
|
||||
|
||||
// AdminLogin is the metadata definition for the admin login event.
|
||||
var AdminLogin = do.EventMetadata{
|
||||
Key: "admin_login",
|
||||
Name: "管理员登录",
|
||||
DefaultTemplate: do.NotificationMessage{
|
||||
Title: "管理员登录提醒",
|
||||
Content: "管理员 {{user.username}} 于 {{time}} 从 IP {{ip}} 登录系统。",
|
||||
Level: consts.DefaultLevelInfo,
|
||||
},
|
||||
Description: "当管理员成功登录系统时触发此通知",
|
||||
}
|
||||
|
||||
// HandleAdminLoggedIn 处理管理员登录事件并触发通知
|
||||
func HandleAdminLoggedIn(ctx context.Context, event contracts.AdminLoggedIn) {
|
||||
if event.User == nil {
|
||||
return
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"user": event.User,
|
||||
"ip": event.IP,
|
||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
DefaultTrigger.Trigger(ctx, AdminLogin, body)
|
||||
}
|
||||
|
||||
// HandleTaskCompleted handles task completion notifications.
|
||||
func HandleTaskCompleted(ctx context.Context, e contracts.TaskCompletedEvent) {
|
||||
events, err := ListActivePushEventsByTaskType(ctx, e.TaskType)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "push_task_completed_listener: failed to query push events for task type %s: %v", e.TaskType, err)
|
||||
return
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"task_id": e.TaskID,
|
||||
"task_name": e.TaskName,
|
||||
"task_type": e.TaskType,
|
||||
"task_status": e.Status,
|
||||
"task_duration": e.Duration,
|
||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"task_error": e.ErrorMsg,
|
||||
"task_result": e.ResultMsg,
|
||||
}
|
||||
|
||||
var payloadMap map[string]any
|
||||
if e.Payload != "" {
|
||||
if err := json.Unmarshal([]byte(e.Payload), &payloadMap); err == nil {
|
||||
body["payload"] = payloadMap
|
||||
ExtractUserFromMap(ctx, payloadMap, body)
|
||||
}
|
||||
}
|
||||
if e.Detail != "" {
|
||||
var detailMap map[string]any
|
||||
if err := json.Unmarshal([]byte(e.Detail), &detailMap); err == nil {
|
||||
body["detail"] = detailMap
|
||||
ExtractUserFromMap(ctx, detailMap, body)
|
||||
}
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
meta := do.EventMetadata{
|
||||
Key: event.EventKey,
|
||||
Name: event.Name,
|
||||
Description: "异步任务执行完毕触发的自动通知",
|
||||
}
|
||||
DefaultTrigger.Trigger(ctx, meta, body)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCustomEvents registers default domain push notification events.
|
||||
func RegisterCustomEvents() {
|
||||
RegisterBuiltInEvent(AdminLogin)
|
||||
}
|
||||
Reference in New Issue
Block a user