From 8395dd501999e2a7176b3b357425c07cf5fd7c9b Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 2 Sep 2026 22:50:04 +0800 Subject: [PATCH] refactor(msg_gateway): restructure and rename message_gateway aligned with custom_example --- .agents/skills/push-notification/SKILL.md | 2 +- backend/cmd/app.go | 4 +- backend/cmd/app_test.go | 4 +- backend/docs/docs.go | 832 +++++++++--------- backend/docs/swagger.json | 832 +++++++++--------- backend/docs/swagger.yaml | 563 ++++++------ .../domain/auth_dependency_ordering_test.go | 8 +- backend/plugins/domain/domain_test.go | 38 +- .../domain/message_gateway/model/admin.go | 46 - .../domain/message_gateway/model/models.go | 242 ----- .../channels/qq/adapter.go | 21 +- .../channels/qq/adapter_test.go | 10 +- .../channels/telegram/adapter.go | 33 +- .../channels/telegram/adapter_test.go | 14 +- .../errs.go => msg_gateway/consts/consts.go} | 67 +- .../controller}/admin.go | 22 +- .../controller}/push_channel.go | 30 +- .../controller}/push_event.go | 31 +- .../controller}/router.go | 3 +- .../controller/user.go} | 35 +- .../repository.go => msg_gateway/dao/dao.go} | 68 +- .../repository => msg_gateway/dao}/push.go | 98 +-- .../dao}/push_test.go | 28 +- .../migrations/postgres/00001_initial.sql | 0 .../migrations/sqlite/00001_initial.sql | 0 .../domain/msg_gateway/model/do/message.go | 124 +++ .../model => msg_gateway/model/do}/push.go | 162 ++-- .../msg_gateway/model/entity/channel.go | 56 ++ .../domain/msg_gateway/model/entity/push.go | 95 ++ .../msg_gateway_test.go} | 12 +- .../pairing_test.go | 2 +- .../plugin.go | 174 ++-- .../plugin_registry_test.go | 8 +- .../plugin_test.go | 20 +- .../push/bark.go | 0 .../push/channels_test.go | 0 .../push/custom.go | 0 .../push/custom_test.go | 0 .../push/dingtalk.go | 0 .../push/discord.go | 0 .../push/email.go | 0 .../push/email_test.go | 0 .../push/lark.go | 0 .../push/push.go | 0 .../push/pushover.go | 0 .../push/slack.go | 0 .../push/telegram.go | 0 .../push/telegram_test.go | 0 .../push/template.go | 0 .../push/template_test.go | 0 .../registry_test.go | 2 +- .../service/admin.go | 119 +-- .../service/dispatch.go | 37 +- .../service/dispatch_test.go | 13 +- .../service/push.go | 305 +++---- .../service/service.go | 105 +-- backend/plugins/domain/system/plugin.go | 5 - 57 files changed, 2150 insertions(+), 2120 deletions(-) delete mode 100644 backend/plugins/domain/message_gateway/model/admin.go delete mode 100644 backend/plugins/domain/message_gateway/model/models.go rename backend/plugins/domain/{message_gateway => msg_gateway}/channels/qq/adapter.go (85%) rename backend/plugins/domain/{message_gateway => msg_gateway}/channels/qq/adapter_test.go (69%) rename backend/plugins/domain/{message_gateway => msg_gateway}/channels/telegram/adapter.go (80%) rename backend/plugins/domain/{message_gateway => msg_gateway}/channels/telegram/adapter_test.go (82%) rename backend/plugins/domain/{message_gateway/errs/errs.go => msg_gateway/consts/consts.go} (65%) rename backend/plugins/domain/{message_gateway/handler => msg_gateway/controller}/admin.go (87%) rename backend/plugins/domain/{message_gateway/handler => msg_gateway/controller}/push_channel.go (81%) rename backend/plugins/domain/{message_gateway/handler => msg_gateway/controller}/push_event.go (88%) rename backend/plugins/domain/{message_gateway/handler => msg_gateway/controller}/router.go (96%) rename backend/plugins/domain/{message_gateway/handler/handlers.go => msg_gateway/controller/user.go} (83%) rename backend/plugins/domain/{message_gateway/repository/repository.go => msg_gateway/dao/dao.go} (69%) rename backend/plugins/domain/{message_gateway/repository => msg_gateway/dao}/push.go (73%) rename backend/plugins/domain/{message_gateway/repository => msg_gateway/dao}/push_test.go (80%) rename backend/plugins/domain/{message_gateway => msg_gateway}/migrations/postgres/00001_initial.sql (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/migrations/sqlite/00001_initial.sql (100%) create mode 100644 backend/plugins/domain/msg_gateway/model/do/message.go rename backend/plugins/domain/{message_gateway/model => msg_gateway/model/do}/push.go (81%) create mode 100644 backend/plugins/domain/msg_gateway/model/entity/channel.go create mode 100644 backend/plugins/domain/msg_gateway/model/entity/push.go rename backend/plugins/domain/{message_gateway/message_gateway_test.go => msg_gateway/msg_gateway_test.go} (66%) rename backend/plugins/domain/{message_gateway => msg_gateway}/pairing_test.go (96%) rename backend/plugins/domain/{message_gateway => msg_gateway}/plugin.go (60%) rename backend/plugins/domain/{message_gateway => msg_gateway}/plugin_registry_test.go (89%) rename backend/plugins/domain/{message_gateway => msg_gateway}/plugin_test.go (71%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/bark.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/channels_test.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/custom.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/custom_test.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/dingtalk.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/discord.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/email.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/email_test.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/lark.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/push.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/pushover.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/slack.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/telegram.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/telegram_test.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/template.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/push/template_test.go (100%) rename backend/plugins/domain/{message_gateway => msg_gateway}/registry_test.go (97%) rename backend/plugins/domain/{message_gateway => msg_gateway}/service/admin.go (65%) rename backend/plugins/domain/{message_gateway => msg_gateway}/service/dispatch.go (79%) rename backend/plugins/domain/{message_gateway => msg_gateway}/service/dispatch_test.go (76%) rename backend/plugins/domain/{message_gateway => msg_gateway}/service/push.go (73%) rename backend/plugins/domain/{message_gateway => msg_gateway}/service/service.go (73%) diff --git a/.agents/skills/push-notification/SKILL.md b/.agents/skills/push-notification/SKILL.md index 5c42e9ee..1d7321a7 100644 --- a/.agents/skills/push-notification/SKILL.md +++ b/.agents/skills/push-notification/SKILL.md @@ -15,7 +15,7 @@ Wavelet 的消息推送机制采用了**元数据驱动 + 统一触发器 + 异 | 目录/包名 | 职责定位 | 包含内容与设计细节 | | :--- | :--- | :--- | -| **`backend/plugins/domain/message_gateway/push/`** | 推送基础设施层 | 静态定义、不依赖系统数据库和任何框架。定义了统一接口 `Pusher` 和多实现(Lark, Webhook, Email 等),提供配置验证及发送功能。 | +| **`backend/plugins/domain/msg_gateway/push/`** | 推送基础设施层 | 静态定义、不依赖系统数据库和任何框架。定义了统一接口 `Pusher` 和多实现(Lark, Webhook, Email 等),提供配置验证及发送功能。 | | **`internal/apps/admin/push/`** | 通知服务与后台任务层 | 包含以下核心文件:
1. `events.go`:定义通知事件的结构模型(`NotificationMessage`, `EventMetadata`)、内置事件的动态注册中心(`BuiltInEvents` 及 `RegisterBuiltInEvent` 函数)以及统一触发器类 `EventTrigger`(包括其底层的派发引擎逻辑)。
2. `tasks.go`:定义 Asynq 后台异步发送任务、处理器 `PushHandler` 及其校验逻辑,并记录推送历史审计。
3. `routers.go`:管理端接口,负责获取事件配置列表和更新配置。 | | **`internal/apps/admin/push/custom_events/`** | 自定义通知事件包 | 事件元数据定义与 push 侧处理逻辑;**一个 Go 文件代表一个事件**。在 `register.go` 统一装配,禁止 `init()` 副作用。 | | **`internal/listener/`** | 域事件分发层 | 核心域发射事件(如 `EmitAdminLoggedIn`),push 在 bootstrap 阶段通过 `OnAdminLoggedIn` 订阅,避免 auth/user 直接依赖 push。 | diff --git a/backend/cmd/app.go b/backend/cmd/app.go index 32fde466..d1cfcb1d 100644 --- a/backend/cmd/app.go +++ b/backend/cmd/app.go @@ -9,7 +9,7 @@ import ( "Wavelet/plugins/domain/admin" "Wavelet/plugins/domain/auth" "Wavelet/plugins/domain/cap" - "Wavelet/plugins/domain/message_gateway" + "Wavelet/plugins/domain/msg_gateway" "Wavelet/plugins/domain/risk_control" "Wavelet/plugins/domain/system" "Wavelet/plugins/domain/upload" @@ -105,7 +105,7 @@ func newWaveletApp(profile core.Profile, opts ...core.AppOption) *core.App { admin.New(), user.New(), auth.New(), - message_gateway.New(), + msg_gateway.New(), risk_control.New(), upload.New(), cap.New(), diff --git a/backend/cmd/app_test.go b/backend/cmd/app_test.go index 2376585f..165e87bc 100644 --- a/backend/cmd/app_test.go +++ b/backend/cmd/app_test.go @@ -82,8 +82,8 @@ func TestNewWaveletAppProfiles(t *testing.T) { _, ok = app.Plugin("user") assert.True(t, ok, "user plugin missing") - _, ok = app.Plugin("message_gateway") - assert.True(t, ok, "message_gateway plugin missing") + _, ok = app.Plugin("msg_gateway") + assert.True(t, ok, "msg_gateway plugin missing") _, ok = app.Plugin("risk_control") assert.True(t, ok, "risk_control plugin missing") diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 4fb284e5..2e512eb7 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1023,7 +1023,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.ChannelDTO" + "$ref": "#/definitions/do.ChannelDTO" } } } @@ -1057,7 +1057,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.CreateChannelRequest" + "$ref": "#/definitions/do.CreateChannelRequest" } } ], @@ -1073,7 +1073,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.ChannelDTO" + "$ref": "#/definitions/do.ChannelDTO" } } } @@ -1118,7 +1118,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.Definition" + "$ref": "#/definitions/do.Definition" } } } @@ -1199,7 +1199,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.UpdateChannelRequest" + "$ref": "#/definitions/do.UpdateChannelRequest" } } ], @@ -1215,7 +1215,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.ChannelDTO" + "$ref": "#/definitions/do.ChannelDTO" } } } @@ -1312,7 +1312,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.PushChannel" + "$ref": "#/definitions/entity.PushChannel" } } } @@ -1346,7 +1346,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.CreatePushChannelRequest" + "$ref": "#/definitions/do.CreatePushChannelRequest" } } ], @@ -1362,7 +1362,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.PushChannel" + "$ref": "#/definitions/entity.PushChannel" } } } @@ -1422,7 +1422,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.TestPushChannelRequest" + "$ref": "#/definitions/do.TestPushChannelRequest" } } ], @@ -1469,7 +1469,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.UpdatePushChannelRequest" + "$ref": "#/definitions/do.UpdatePushChannelRequest" } } ], @@ -1485,7 +1485,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.PushChannel" + "$ref": "#/definitions/entity.PushChannel" } } } @@ -1557,7 +1557,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.PushEvent" + "$ref": "#/definitions/entity.PushEvent" } } } @@ -1591,7 +1591,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.CreatePushEventRequest" + "$ref": "#/definitions/do.CreatePushEventRequest" } } ], @@ -1607,7 +1607,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.PushEvent" + "$ref": "#/definitions/entity.PushEvent" } } } @@ -1674,7 +1674,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.UpdatePushEventRequest" + "$ref": "#/definitions/do.UpdatePushEventRequest" } } ], @@ -1866,7 +1866,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.TestPushRequest" + "$ref": "#/definitions/do.TestPushRequest" } } ], @@ -4588,7 +4588,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.BindingDTO" + "$ref": "#/definitions/do.BindingDTO" } } } @@ -4628,7 +4628,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.BindRequest" + "$ref": "#/definitions/do.BindRequest" } } ], @@ -4644,7 +4644,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.BindingDTO" + "$ref": "#/definitions/do.BindingDTO" } } } @@ -4741,7 +4741,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.PublicChannelDTO" + "$ref": "#/definitions/do.PublicChannelDTO" } } } @@ -6361,6 +6361,399 @@ const docTemplate = `{ } } }, + "do.BindRequest": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "code": { + "type": "string" + } + } + }, + "do.BindingDTO": { + "type": "object", + "properties": { + "channel_id": { + "type": "string", + "example": "0" + }, + "channel_name": { + "type": "string" + }, + "channel_type": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string", + "example": "0" + }, + "platform_user_id": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + } + } + }, + "do.ChannelDTO": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "extra": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "id": { + "type": "string", + "example": "0" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string", + "example": "0" + }, + "owner_scope": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "do.CreateChannelRequest": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "extra": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "do.CreatePushChannelRequest": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "do.CreatePushEventRequest": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "event_key": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "task_type": { + "type": "string" + }, + "template": { + "type": "string" + } + } + }, + "do.Definition": { + "type": "object", + "properties": { + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/do.Field" + } + }, + "type": { + "type": "string" + } + } + }, + "do.Field": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "type": { + "type": "string" + } + } + }, + "do.PublicChannelDTO": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "0" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "do.TestPushChannelRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "target": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "do.TestPushRequest": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/push.Config" + }, + "target": { + "type": "string" + } + } + }, + "do.UpdateChannelRequest": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "extra": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "do.UpdatePushChannelRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "do.UpdatePushEventRequest": { + "type": "object", + "required": [ + "template" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "type": "string" + } + } + }, + "entity.PushChannel": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "entity.PushEvent": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "event_key": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "task_type": { + "type": "string" + }, + "template": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handler.batchDownloadRequest": { "type": "object", "required": [ @@ -6562,46 +6955,6 @@ const docTemplate = `{ } } }, - "model.BindRequest": { - "type": "object", - "properties": { - "channel_id": { - "type": "string" - }, - "code": { - "type": "string" - } - } - }, - "model.BindingDTO": { - "type": "object", - "properties": { - "channel_id": { - "type": "string", - "example": "0" - }, - "channel_name": { - "type": "string" - }, - "channel_type": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string", - "example": "0" - }, - "platform_user_id": { - "type": "string" - }, - "user_id": { - "type": "string", - "example": "0" - } - } - }, "model.BrowserItem": { "type": "object", "properties": { @@ -6613,128 +6966,6 @@ const docTemplate = `{ } } }, - "model.ChannelDTO": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "extra": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "id": { - "type": "string", - "example": "0" - }, - "name": { - "type": "string" - }, - "owner_id": { - "type": "string", - "example": "0" - }, - "owner_scope": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "model.CreateChannelRequest": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "extra": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "model.CreatePushChannelRequest": { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "other": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.CreatePushEventRequest": { - "type": "object", - "properties": { - "channels": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "event_key": { - "type": "string" - }, - "targets": { - "type": "array", - "items": { - "type": "string" - } - }, - "task_type": { - "type": "string" - }, - "template": { - "type": "string" - } - } - }, "model.CreateScheduleRequest": { "type": "object", "required": [ @@ -6901,20 +7132,6 @@ const docTemplate = `{ } } }, - "model.Definition": { - "type": "object", - "properties": { - "fields": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Field" - } - }, - "type": { - "type": "string" - } - } - }, "model.DispatchTaskRequest": { "type": "object", "required": [ @@ -6977,20 +7194,6 @@ const docTemplate = `{ } } }, - "model.Field": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "type": { - "type": "string" - } - } - }, "model.ListUsersResponse": { "type": "object", "properties": { @@ -7069,97 +7272,6 @@ const docTemplate = `{ } } }, - "model.PublicChannelDTO": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "0" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "model.PushChannel": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "other": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.PushEvent": { - "type": "object", - "properties": { - "channels": { - "type": "array", - "items": { - "type": "string" - } - }, - "created_at": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "event_key": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "targets": { - "type": "array", - "items": { - "type": "string" - } - }, - "task_type": { - "type": "string" - }, - "template": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, "model.Schedule": { "type": "object", "properties": { @@ -7415,43 +7527,6 @@ const docTemplate = `{ } } }, - "model.TestPushChannelRequest": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "other": { - "type": "string" - }, - "target": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.TestPushRequest": { - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "$ref": "#/definitions/push.Config" - }, - "target": { - "type": "string" - } - } - }, "model.TestSMTPRequest": { "type": "object", "required": [ @@ -7545,81 +7620,6 @@ const docTemplate = `{ } } }, - "model.UpdateChannelRequest": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "extra": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - } - } - }, - "model.UpdatePushChannelRequest": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "other": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.UpdatePushEventRequest": { - "type": "object", - "required": [ - "template" - ], - "properties": { - "channels": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "targets": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "string" - } - } - }, "model.UpdateScheduleRequest": { "type": "object", "required": [ @@ -7942,6 +7942,10 @@ const docTemplate = `{ "description": "AppID 或 SMTP 用户名", "type": "string" }, + "other": { + "description": "附加配置 (如 ChatID / UserKey / 扩展 JSON)", + "type": "string" + }, "secret": { "description": "签名密钥或 SMTP 密码/Token", "type": "string" diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 97b0bf07..894af81a 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1016,7 +1016,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.ChannelDTO" + "$ref": "#/definitions/do.ChannelDTO" } } } @@ -1050,7 +1050,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.CreateChannelRequest" + "$ref": "#/definitions/do.CreateChannelRequest" } } ], @@ -1066,7 +1066,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.ChannelDTO" + "$ref": "#/definitions/do.ChannelDTO" } } } @@ -1111,7 +1111,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.Definition" + "$ref": "#/definitions/do.Definition" } } } @@ -1192,7 +1192,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.UpdateChannelRequest" + "$ref": "#/definitions/do.UpdateChannelRequest" } } ], @@ -1208,7 +1208,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.ChannelDTO" + "$ref": "#/definitions/do.ChannelDTO" } } } @@ -1305,7 +1305,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.PushChannel" + "$ref": "#/definitions/entity.PushChannel" } } } @@ -1339,7 +1339,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.CreatePushChannelRequest" + "$ref": "#/definitions/do.CreatePushChannelRequest" } } ], @@ -1355,7 +1355,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.PushChannel" + "$ref": "#/definitions/entity.PushChannel" } } } @@ -1415,7 +1415,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.TestPushChannelRequest" + "$ref": "#/definitions/do.TestPushChannelRequest" } } ], @@ -1462,7 +1462,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.UpdatePushChannelRequest" + "$ref": "#/definitions/do.UpdatePushChannelRequest" } } ], @@ -1478,7 +1478,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.PushChannel" + "$ref": "#/definitions/entity.PushChannel" } } } @@ -1550,7 +1550,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.PushEvent" + "$ref": "#/definitions/entity.PushEvent" } } } @@ -1584,7 +1584,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.CreatePushEventRequest" + "$ref": "#/definitions/do.CreatePushEventRequest" } } ], @@ -1600,7 +1600,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.PushEvent" + "$ref": "#/definitions/entity.PushEvent" } } } @@ -1667,7 +1667,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.UpdatePushEventRequest" + "$ref": "#/definitions/do.UpdatePushEventRequest" } } ], @@ -1859,7 +1859,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.TestPushRequest" + "$ref": "#/definitions/do.TestPushRequest" } } ], @@ -4581,7 +4581,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.BindingDTO" + "$ref": "#/definitions/do.BindingDTO" } } } @@ -4621,7 +4621,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/model.BindRequest" + "$ref": "#/definitions/do.BindRequest" } } ], @@ -4637,7 +4637,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/model.BindingDTO" + "$ref": "#/definitions/do.BindingDTO" } } } @@ -4734,7 +4734,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/model.PublicChannelDTO" + "$ref": "#/definitions/do.PublicChannelDTO" } } } @@ -6354,6 +6354,399 @@ } } }, + "do.BindRequest": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "code": { + "type": "string" + } + } + }, + "do.BindingDTO": { + "type": "object", + "properties": { + "channel_id": { + "type": "string", + "example": "0" + }, + "channel_name": { + "type": "string" + }, + "channel_type": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string", + "example": "0" + }, + "platform_user_id": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + } + } + }, + "do.ChannelDTO": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "extra": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "id": { + "type": "string", + "example": "0" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string", + "example": "0" + }, + "owner_scope": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "do.CreateChannelRequest": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "extra": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "do.CreatePushChannelRequest": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "do.CreatePushEventRequest": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "event_key": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "task_type": { + "type": "string" + }, + "template": { + "type": "string" + } + } + }, + "do.Definition": { + "type": "object", + "properties": { + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/do.Field" + } + }, + "type": { + "type": "string" + } + } + }, + "do.Field": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "type": { + "type": "string" + } + } + }, + "do.PublicChannelDTO": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "0" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "do.TestPushChannelRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "target": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "do.TestPushRequest": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/push.Config" + }, + "target": { + "type": "string" + } + } + }, + "do.UpdateChannelRequest": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "extra": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "do.UpdatePushChannelRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "do.UpdatePushEventRequest": { + "type": "object", + "required": [ + "template" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "type": "string" + } + } + }, + "entity.PushChannel": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "entity.PushEvent": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "event_key": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "task_type": { + "type": "string" + }, + "template": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handler.batchDownloadRequest": { "type": "object", "required": [ @@ -6555,46 +6948,6 @@ } } }, - "model.BindRequest": { - "type": "object", - "properties": { - "channel_id": { - "type": "string" - }, - "code": { - "type": "string" - } - } - }, - "model.BindingDTO": { - "type": "object", - "properties": { - "channel_id": { - "type": "string", - "example": "0" - }, - "channel_name": { - "type": "string" - }, - "channel_type": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string", - "example": "0" - }, - "platform_user_id": { - "type": "string" - }, - "user_id": { - "type": "string", - "example": "0" - } - } - }, "model.BrowserItem": { "type": "object", "properties": { @@ -6606,128 +6959,6 @@ } } }, - "model.ChannelDTO": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "extra": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "id": { - "type": "string", - "example": "0" - }, - "name": { - "type": "string" - }, - "owner_id": { - "type": "string", - "example": "0" - }, - "owner_scope": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "model.CreateChannelRequest": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "extra": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "model.CreatePushChannelRequest": { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "other": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.CreatePushEventRequest": { - "type": "object", - "properties": { - "channels": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "event_key": { - "type": "string" - }, - "targets": { - "type": "array", - "items": { - "type": "string" - } - }, - "task_type": { - "type": "string" - }, - "template": { - "type": "string" - } - } - }, "model.CreateScheduleRequest": { "type": "object", "required": [ @@ -6894,20 +7125,6 @@ } } }, - "model.Definition": { - "type": "object", - "properties": { - "fields": { - "type": "array", - "items": { - "$ref": "#/definitions/model.Field" - } - }, - "type": { - "type": "string" - } - } - }, "model.DispatchTaskRequest": { "type": "object", "required": [ @@ -6970,20 +7187,6 @@ } } }, - "model.Field": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "type": { - "type": "string" - } - } - }, "model.ListUsersResponse": { "type": "object", "properties": { @@ -7062,97 +7265,6 @@ } } }, - "model.PublicChannelDTO": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "0" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "model.PushChannel": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "other": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.PushEvent": { - "type": "object", - "properties": { - "channels": { - "type": "array", - "items": { - "type": "string" - } - }, - "created_at": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "event_key": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "targets": { - "type": "array", - "items": { - "type": "string" - } - }, - "task_type": { - "type": "string" - }, - "template": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, "model.Schedule": { "type": "object", "properties": { @@ -7408,43 +7520,6 @@ } } }, - "model.TestPushChannelRequest": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "other": { - "type": "string" - }, - "target": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.TestPushRequest": { - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "$ref": "#/definitions/push.Config" - }, - "target": { - "type": "string" - } - } - }, "model.TestSMTPRequest": { "type": "object", "required": [ @@ -7538,81 +7613,6 @@ } } }, - "model.UpdateChannelRequest": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "extra": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - } - } - }, - "model.UpdatePushChannelRequest": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "other": { - "type": "string" - }, - "token": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.UpdatePushEventRequest": { - "type": "object", - "required": [ - "template" - ], - "properties": { - "channels": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "targets": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "string" - } - } - }, "model.UpdateScheduleRequest": { "type": "object", "required": [ @@ -7935,6 +7935,10 @@ "description": "AppID 或 SMTP 用户名", "type": "string" }, + "other": { + "description": "附加配置 (如 ChatID / UserKey / 扩展 JSON)", + "type": "string" + }, "secret": { "description": "签名密钥或 SMTP 密码/Token", "type": "string" diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 284d2e54..53bf3835 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -204,6 +204,265 @@ definitions: ttl_minutes: type: integer type: object + do.BindRequest: + properties: + channel_id: + type: string + code: + type: string + type: object + do.BindingDTO: + properties: + channel_id: + example: "0" + type: string + channel_name: + type: string + channel_type: + type: string + created_at: + type: string + id: + example: "0" + type: string + platform_user_id: + type: string + user_id: + example: "0" + type: string + type: object + do.ChannelDTO: + properties: + credentials: + additionalProperties: + type: string + type: object + enabled: + type: boolean + extra: + additionalProperties: + type: string + type: object + id: + example: "0" + type: string + name: + type: string + owner_id: + example: "0" + type: string + owner_scope: + type: string + type: + type: string + type: object + do.CreateChannelRequest: + properties: + credentials: + additionalProperties: + type: string + type: object + enabled: + type: boolean + extra: + additionalProperties: + type: string + type: object + name: + type: string + type: + type: string + type: object + do.CreatePushChannelRequest: + properties: + description: + type: string + enabled: + type: boolean + name: + type: string + other: + type: string + token: + type: string + type: + type: string + url: + type: string + required: + - name + - type + type: object + do.CreatePushEventRequest: + properties: + channels: + items: + type: string + type: array + enabled: + type: boolean + event_key: + type: string + targets: + items: + type: string + type: array + task_type: + type: string + template: + type: string + type: object + do.Definition: + properties: + fields: + items: + $ref: '#/definitions/do.Field' + type: array + type: + type: string + type: object + do.Field: + properties: + key: + type: string + required: + type: boolean + type: + type: string + type: object + do.PublicChannelDTO: + properties: + id: + example: "0" + type: string + name: + type: string + type: + type: string + type: object + do.TestPushChannelRequest: + properties: + name: + type: string + other: + type: string + target: + type: string + token: + type: string + type: + type: string + url: + type: string + type: object + do.TestPushRequest: + properties: + config: + $ref: '#/definitions/push.Config' + target: + type: string + required: + - config + type: object + do.UpdateChannelRequest: + properties: + credentials: + additionalProperties: + type: string + type: object + enabled: + type: boolean + extra: + additionalProperties: + type: string + type: object + name: + type: string + type: object + do.UpdatePushChannelRequest: + properties: + description: + type: string + enabled: + type: boolean + other: + type: string + token: + type: string + type: + type: string + url: + type: string + required: + - type + type: object + do.UpdatePushEventRequest: + properties: + channels: + items: + type: string + type: array + enabled: + type: boolean + targets: + items: + type: string + type: array + template: + type: string + required: + - template + type: object + entity.PushChannel: + properties: + created_at: + type: string + description: + type: string + enabled: + type: boolean + id: + type: integer + name: + type: string + other: + type: string + token: + type: string + type: + type: string + updated_at: + type: string + url: + type: string + type: object + entity.PushEvent: + properties: + channels: + items: + type: string + type: array + created_at: + type: string + enabled: + type: boolean + event_key: + type: string + id: + type: integer + name: + type: string + targets: + items: + type: string + type: array + task_type: + type: string + template: + type: string + updated_at: + type: string + type: object handler.batchDownloadRequest: properties: ids: @@ -337,33 +596,6 @@ definitions: total: type: integer type: object - model.BindRequest: - properties: - channel_id: - type: string - code: - type: string - type: object - model.BindingDTO: - properties: - channel_id: - example: "0" - type: string - channel_name: - type: string - channel_type: - type: string - created_at: - type: string - id: - example: "0" - type: string - platform_user_id: - type: string - user_id: - example: "0" - type: string - type: object model.BrowserItem: properties: browser: @@ -371,87 +603,6 @@ definitions: count: type: integer type: object - model.ChannelDTO: - properties: - credentials: - additionalProperties: - type: string - type: object - enabled: - type: boolean - extra: - additionalProperties: - type: string - type: object - id: - example: "0" - type: string - name: - type: string - owner_id: - example: "0" - type: string - owner_scope: - type: string - type: - type: string - type: object - model.CreateChannelRequest: - properties: - credentials: - additionalProperties: - type: string - type: object - enabled: - type: boolean - extra: - additionalProperties: - type: string - type: object - name: - type: string - type: - type: string - type: object - model.CreatePushChannelRequest: - properties: - description: - type: string - enabled: - type: boolean - name: - type: string - other: - type: string - token: - type: string - type: - type: string - url: - type: string - required: - - name - - type - type: object - model.CreatePushEventRequest: - properties: - channels: - items: - type: string - type: array - enabled: - type: boolean - event_key: - type: string - targets: - items: - type: string - type: array - task_type: - type: string - template: - type: string - type: object model.CreateScheduleRequest: properties: cron: @@ -569,15 +720,6 @@ definitions: version: type: string type: object - model.Definition: - properties: - fields: - items: - $ref: '#/definitions/model.Field' - type: array - type: - type: string - type: object model.DispatchTaskRequest: properties: end_time: @@ -619,15 +761,6 @@ definitions: description: '"select" 或 "exec"' type: string type: object - model.Field: - properties: - key: - type: string - required: - type: boolean - type: - type: string - type: object model.ListUsersResponse: properties: total: @@ -679,66 +812,6 @@ definitions: description: 用于加载更早日志的 cursor type: integer type: object - model.PublicChannelDTO: - properties: - id: - example: "0" - type: string - name: - type: string - type: - type: string - type: object - model.PushChannel: - properties: - created_at: - type: string - description: - type: string - enabled: - type: boolean - id: - type: integer - name: - type: string - other: - type: string - token: - type: string - type: - type: string - updated_at: - type: string - url: - type: string - type: object - model.PushEvent: - properties: - channels: - items: - type: string - type: array - created_at: - type: string - enabled: - type: boolean - event_key: - type: string - id: - type: integer - name: - type: string - targets: - items: - type: string - type: array - task_type: - type: string - template: - type: string - updated_at: - type: string - type: object model.Schedule: properties: created_at: @@ -910,30 +983,6 @@ definitions: updated_at: type: string type: object - model.TestPushChannelRequest: - properties: - name: - type: string - other: - type: string - target: - type: string - token: - type: string - type: - type: string - url: - type: string - type: object - model.TestPushRequest: - properties: - config: - $ref: '#/definitions/push.Config' - target: - type: string - required: - - config - type: object model.TestSMTPRequest: properties: smtp_host: @@ -998,55 +1047,6 @@ definitions: - max_size_mb - ttl_minutes type: object - model.UpdateChannelRequest: - properties: - credentials: - additionalProperties: - type: string - type: object - enabled: - type: boolean - extra: - additionalProperties: - type: string - type: object - name: - type: string - type: object - model.UpdatePushChannelRequest: - properties: - description: - type: string - enabled: - type: boolean - other: - type: string - token: - type: string - type: - type: string - url: - type: string - required: - - type - type: object - model.UpdatePushEventRequest: - properties: - channels: - items: - type: string - type: array - enabled: - type: boolean - targets: - items: - type: string - type: array - template: - type: string - required: - - template - type: object model.UpdateScheduleRequest: properties: cron: @@ -1269,6 +1269,9 @@ definitions: key: description: AppID 或 SMTP 用户名 type: string + other: + description: 附加配置 (如 ChatID / UserKey / 扩展 JSON) + type: string secret: description: 签名密钥或 SMTP 密码/Token type: string @@ -2006,7 +2009,7 @@ paths: - properties: data: items: - $ref: '#/definitions/model.ChannelDTO' + $ref: '#/definitions/do.ChannelDTO' type: array type: object security: @@ -2024,7 +2027,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.CreateChannelRequest' + $ref: '#/definitions/do.CreateChannelRequest' produces: - application/json responses: @@ -2035,7 +2038,7 @@ paths: - $ref: '#/definitions/response.Any' - properties: data: - $ref: '#/definitions/model.ChannelDTO' + $ref: '#/definitions/do.ChannelDTO' type: object "400": description: Bad Request @@ -2086,7 +2089,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.UpdateChannelRequest' + $ref: '#/definitions/do.UpdateChannelRequest' produces: - application/json responses: @@ -2097,7 +2100,7 @@ paths: - $ref: '#/definitions/response.Any' - properties: data: - $ref: '#/definitions/model.ChannelDTO' + $ref: '#/definitions/do.ChannelDTO' type: object "400": description: Bad Request @@ -2155,7 +2158,7 @@ paths: - properties: data: items: - $ref: '#/definitions/model.Definition' + $ref: '#/definitions/do.Definition' type: array type: object security: @@ -2177,7 +2180,7 @@ paths: - properties: data: items: - $ref: '#/definitions/model.PushChannel' + $ref: '#/definitions/entity.PushChannel' type: array type: object security: @@ -2195,7 +2198,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.CreatePushChannelRequest' + $ref: '#/definitions/do.CreatePushChannelRequest' produces: - application/json responses: @@ -2206,7 +2209,7 @@ paths: - $ref: '#/definitions/response.Any' - properties: data: - $ref: '#/definitions/model.PushChannel' + $ref: '#/definitions/entity.PushChannel' type: object security: - SessionCookie: [] @@ -2251,7 +2254,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.UpdatePushChannelRequest' + $ref: '#/definitions/do.UpdatePushChannelRequest' produces: - application/json responses: @@ -2262,7 +2265,7 @@ paths: - $ref: '#/definitions/response.Any' - properties: data: - $ref: '#/definitions/model.PushChannel' + $ref: '#/definitions/entity.PushChannel' type: object security: - SessionCookie: [] @@ -2295,7 +2298,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.TestPushChannelRequest' + $ref: '#/definitions/do.TestPushChannelRequest' produces: - application/json responses: @@ -2322,7 +2325,7 @@ paths: - properties: data: items: - $ref: '#/definitions/model.PushEvent' + $ref: '#/definitions/entity.PushEvent' type: array type: object security: @@ -2340,7 +2343,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.CreatePushEventRequest' + $ref: '#/definitions/do.CreatePushEventRequest' produces: - application/json responses: @@ -2351,7 +2354,7 @@ paths: - $ref: '#/definitions/response.Any' - properties: data: - $ref: '#/definitions/model.PushEvent' + $ref: '#/definitions/entity.PushEvent' type: object security: - SessionCookie: [] @@ -2399,7 +2402,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.UpdatePushEventRequest' + $ref: '#/definitions/do.UpdatePushEventRequest' produces: - application/json responses: @@ -2501,7 +2504,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.TestPushRequest' + $ref: '#/definitions/do.TestPushRequest' produces: - application/json responses: @@ -4141,7 +4144,7 @@ paths: - properties: data: items: - $ref: '#/definitions/model.BindingDTO' + $ref: '#/definitions/do.BindingDTO' type: array type: object "401": @@ -4164,7 +4167,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/model.BindRequest' + $ref: '#/definitions/do.BindRequest' produces: - application/json responses: @@ -4175,7 +4178,7 @@ paths: - $ref: '#/definitions/response.Any' - properties: data: - $ref: '#/definitions/model.BindingDTO' + $ref: '#/definitions/do.BindingDTO' type: object "400": description: Bad Request @@ -4233,7 +4236,7 @@ paths: - properties: data: items: - $ref: '#/definitions/model.PublicChannelDTO' + $ref: '#/definitions/do.PublicChannelDTO' type: array type: object "401": diff --git a/backend/plugins/domain/auth_dependency_ordering_test.go b/backend/plugins/domain/auth_dependency_ordering_test.go index 5e4df016..1dfcf1b9 100644 --- a/backend/plugins/domain/auth_dependency_ordering_test.go +++ b/backend/plugins/domain/auth_dependency_ordering_test.go @@ -19,7 +19,7 @@ import ( "Wavelet/core/contracts" "Wavelet/core/extpoints" "Wavelet/plugins/domain/admin" - "Wavelet/plugins/domain/message_gateway" + "Wavelet/plugins/domain/msg_gateway" "Wavelet/plugins/domain/user" ) @@ -122,7 +122,7 @@ func TestRoutesMountedBeforeAuthServiceAreGuarded(t *testing.T) { core.WithPlugins( dbProvider(), user.New(), - message_gateway.New(), + msg_gateway.New(), authProvider(), ), ) @@ -147,7 +147,7 @@ func TestAuthConsumersDeclareAuthDependency(t *testing.T) { deps []reflect.Type }{ {"user", user.New().Inject()}, - {"message_gateway", message_gateway.New().Inject()}, + {"msg_gateway", msg_gateway.New().Inject()}, } { t.Run(tc.name, func(t *testing.T) { assert.Contains(t, tc.deps, want, @@ -194,7 +194,7 @@ func TestAuthGuardFailsClosed(t *testing.T) { prefix string }{ {"user", user.New().Apply, http.MethodPost, "/api/v1/user/change-password"}, - {"message_gateway", message_gateway.New().Apply, http.MethodGet, "/api/v1/message-gateway"}, + {"msg_gateway", msg_gateway.New().Apply, http.MethodGet, "/api/v1/message-gateway"}, {"admin", admin.New().Apply, http.MethodGet, "/api/v1/admin"}, } diff --git a/backend/plugins/domain/domain_test.go b/backend/plugins/domain/domain_test.go index a5b3ab13..8bc87a52 100644 --- a/backend/plugins/domain/domain_test.go +++ b/backend/plugins/domain/domain_test.go @@ -9,7 +9,7 @@ import ( "Wavelet/pkg/idgen" "Wavelet/plugins/domain/admin" "Wavelet/plugins/domain/auth" - "Wavelet/plugins/domain/message_gateway" + "Wavelet/plugins/domain/msg_gateway" "Wavelet/plugins/domain/risk_control" "Wavelet/plugins/domain/system" "Wavelet/plugins/domain/user" @@ -47,13 +47,13 @@ func setupTestDB(t *testing.T) *gorm.DB { &user.AccessToken{}, &auth.AuthSource{}, &auth.ExternalAccount{}, - &message_gateway.MessageChannel{}, - &message_gateway.MessageBinding{}, - &message_gateway.MessagePairingCode{}, + &msg_gateway.MessageChannel{}, + &msg_gateway.MessageBinding{}, + &msg_gateway.MessagePairingCode{}, &admin.SystemConfig{}, - &message_gateway.PushChannel{}, - &message_gateway.PushEvent{}, - &message_gateway.PushHistory{}, + &msg_gateway.PushChannel{}, + &msg_gateway.PushEvent{}, + &msg_gateway.PushHistory{}, )) db.SetDB(testDB) @@ -262,15 +262,15 @@ func TestMessageGatewayPlugin(t *testing.T) { require.NoError(t, cache.New().Apply(ctx)) require.NoError(t, logger.New().Apply(ctx)) - p := message_gateway.New() - assert.Equal(t, "message_gateway", p.Name()) - assert.Equal(t, "message_gateway", p.Manifest().Name) + p := msg_gateway.New() + assert.Equal(t, "msg_gateway", p.Name()) + assert.Equal(t, "msg_gateway", p.Manifest().Name) require.NoError(t, p.Apply(ctx)) // 1. Migrations - entry, ok := ctx.Migrations().Get("message_gateway") + entry, ok := ctx.Migrations().Get("msg_gateway") require.True(t, ok) - assert.Equal(t, "message_gateway", entry.PluginID) + assert.Equal(t, "msg_gateway", entry.PluginID) // 2. Routes routes := ctx.Router().Routes() @@ -287,24 +287,24 @@ func TestMessageGatewayPlugin(t *testing.T) { assert.True(t, hasBindings) // 3. Tasks & Schedules - taskDef, ok := ctx.Tasks().Get("message_gateway:push_notification") + taskDef, ok := ctx.Tasks().Get("msg_gateway:push_notification") require.True(t, ok) assert.Equal(t, 3, taskDef.Retry) - schedDef, ok := ctx.Schedules().Get("message_gateway:cleanup_pairing_codes") + schedDef, ok := ctx.Schedules().Get("msg_gateway:cleanup_pairing_codes") require.True(t, ok) assert.Equal(t, "*/10 * * * *", schedDef.Spec) // 4. EventBus Trigger - var receivedEvent message_gateway.PushNotificationEvent + var receivedEvent msg_gateway.PushNotificationEvent var eventFired bool - ctx.Events().On("notification:push", func(c context.Context, e message_gateway.PushNotificationEvent) error { + ctx.Events().On("notification:push", func(c context.Context, e msg_gateway.PushNotificationEvent) error { eventFired = true receivedEvent = e return nil }) - err := ctx.Events().Emit(context.Background(), "notification:push", message_gateway.PushNotificationEvent{ + err := ctx.Events().Emit(context.Background(), "notification:push", msg_gateway.PushNotificationEvent{ UserID: 99, Channel: "telegram", Title: "System Alert", @@ -317,7 +317,7 @@ func TestMessageGatewayPlugin(t *testing.T) { assert.Equal(t, "System Alert", receivedEvent.Title) // 5. Settings - schema, ok := ctx.Settings().Get("message_gateway.pairing_code_expiry_minutes") + schema, ok := ctx.Settings().Get("msg_gateway.pairing_code_expiry_minutes") require.True(t, ok) assert.Equal(t, 15, schema.Default) } @@ -480,7 +480,7 @@ func TestAllDomainPluginsCombined(t *testing.T) { // Apply Domain plugins require.NoError(t, auth.New().Apply(ctx)) require.NoError(t, user.New().Apply(ctx)) - require.NoError(t, message_gateway.New().Apply(ctx)) + require.NoError(t, msg_gateway.New().Apply(ctx)) require.NoError(t, risk_control.New().Apply(ctx)) require.NoError(t, admin.New().Apply(ctx)) diff --git a/backend/plugins/domain/message_gateway/model/admin.go b/backend/plugins/domain/message_gateway/model/admin.go deleted file mode 100644 index 0a8a7daf..00000000 --- a/backend/plugins/domain/message_gateway/model/admin.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package model - -// Field is one admin form field. -type Field struct { - Key string `json:"key"` - Type string `json:"type"` - Required bool `json:"required"` -} - -// Definition describes a channel type form. -type Definition struct { - Type string `json:"type"` - Fields []Field `json:"fields"` -} - -// ChannelDTO represents a channel for admin consumption. -type ChannelDTO struct { - ID uint64 `json:"id,string"` - Name string `json:"name"` - Type string `json:"type"` - OwnerScope string `json:"owner_scope"` - OwnerID *uint64 `json:"owner_id,string,omitempty"` - Enabled bool `json:"enabled"` - Credentials map[string]string `json:"credentials"` - Extra map[string]string `json:"extra"` -} - -// CreateChannelRequest is admin create payload. -type CreateChannelRequest struct { - Name string `json:"name"` - Type string `json:"type"` - Enabled *bool `json:"enabled"` - Credentials map[string]string `json:"credentials"` - Extra map[string]string `json:"extra"` -} - -// UpdateChannelRequest is admin update payload. -type UpdateChannelRequest struct { - Name string `json:"name"` - Enabled *bool `json:"enabled"` - Credentials map[string]string `json:"credentials"` - Extra map[string]string `json:"extra"` -} diff --git a/backend/plugins/domain/message_gateway/model/models.go b/backend/plugins/domain/message_gateway/model/models.go deleted file mode 100644 index f5064736..00000000 --- a/backend/plugins/domain/message_gateway/model/models.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -// Package model defines the domain entities, DTOs, and schemas for message_gateway. -package model - -import ( - "Wavelet/plugins/domain/message_gateway/errs" - "errors" - "strings" - "time" -) - -// Channel type and scope constants. -const ( - ChannelTypeTelegram = "telegram" - ChannelTypeQQ = "qq" - MessageChannelTypeTelegram = "telegram" - MessageChannelTypeQQ = "qq" - MessageOwnerScopeSystem = "system" - - TypeCustom = "custom" - TypeEmail = "email" - TypeTelegram = "telegram" -) - -// Capability describes what an adapter can send and receive. -type Capability struct { - Text bool - Image bool - File bool - Reply bool - Group bool -} - -// ChannelConfig is the decrypted runtime config passed to a factory. -type ChannelConfig struct { - ID uint64 - Type string - Name string - Credentials map[string]string - Extra map[string]string -} - -// Recipient is the outbound destination on a platform. -type Recipient struct { - ChatID string - PlatformUserID string -} - -// Attachment is a downloaded inbound file sitting on local disk. -type Attachment struct { - Path string - FileName string - MIME string - Error string -} - -// InboundMessage is a normalized private-chat message. -type InboundMessage struct { - ChannelID uint64 - ChannelType string - PlatformUserID string - ChatID string - MessageID string - Text string - Attachments []Attachment - BindingUserID *uint64 -} - -// OutboundMessage is a reply or probe send. -type OutboundMessage struct { - Text string - ReplyToID string - Attachments []Attachment -} - -// MessageChannel is an admin-configured messaging adapter. -type MessageChannel struct { - ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` - Type string `json:"type" gorm:"size:32;not null"` - Name string `json:"name" gorm:"size:64;not null"` - OwnerScope string `json:"owner_scope" gorm:"size:32;not null;default:'system'"` - OwnerID *uint64 `json:"owner_id,omitempty"` - Credentials string `json:"credentials" gorm:"type:text;not null"` - Extra string `json:"extra" gorm:"type:text"` - Enabled bool `json:"enabled" gorm:"default:false;not null"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` -} - -// TableName 表名 -func (MessageChannel) TableName() string { - return "w_message_channels" -} - -// MessageBinding maps a platform user to a Wavelet user on one channel. -type MessageBinding struct { - ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` - ChannelID uint64 `json:"channel_id" gorm:"not null;index"` - PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"` - UserID uint64 `json:"user_id" gorm:"not null;index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` -} - -// TableName 表名 -func (MessageBinding) TableName() string { - return "w_message_bindings" -} - -// MessagePairingCode is a one-time bind code. -type MessagePairingCode struct { - ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` - Code string `json:"code" gorm:"size:32;uniqueIndex;not null"` - ChannelID uint64 `json:"channel_id" gorm:"not null;index"` - PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"` - UserID uint64 `json:"user_id" gorm:"not null;index"` - ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` -} - -// TableName 表名 -func (MessagePairingCode) TableName() string { - return "w_message_pairing_codes" -} - -// PushChannel 消息通道模型 -type PushChannel struct { - ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` - Name string `json:"name" gorm:"size:100;not null"` - Description string `json:"description" gorm:"size:255"` - Type string `json:"type" gorm:"size:50;not null;index"` - URL string `json:"url" gorm:"type:text"` - Token string `json:"token" gorm:"type:text"` - Other string `json:"other" gorm:"type:text"` - Enabled bool `json:"enabled" gorm:"index;not null;default:true"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` -} - -// TableName 指定 GORM 表名 -func (PushChannel) TableName() string { - return "w_push_channels" -} - -// Validate 验证与标准化字段 -func (c *PushChannel) Validate() error { - c.Name = strings.TrimSpace(c.Name) - if c.Name == "" { - return errors.New(errs.ErrChannelNameRequired) - } - c.Type = strings.TrimSpace(c.Type) - if c.Type == "" { - return errors.New(errs.ErrChannelTypeRequired) - } - return nil -} - -// PushEvent 系统通知事件模型 -type PushEvent struct { - ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` - EventKey string `json:"event_key" gorm:"uniqueIndex;size:80;not null"` - Name string `json:"name" gorm:"size:100;not null"` - TaskType string `json:"task_type" gorm:"size:100;index;not null;default:''"` - Channels []string `json:"channels" gorm:"type:text;serializer:json;not null"` - Targets []string `json:"targets" gorm:"type:text;serializer:json;not null"` - Template string `json:"template" gorm:"type:text;not null"` - Enabled bool `json:"enabled" gorm:"index;not null;default:false"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` -} - -// TableName 指定 GORM 表名 -func (PushEvent) TableName() string { - return "w_push_events" -} - -// Validate 验证 PushEvent 实体字段 -func (e *PushEvent) Validate() error { - e.EventKey = strings.TrimSpace(e.EventKey) - if e.EventKey == "" { - return errors.New(errs.ErrEventKeyRequired) - } - e.Name = strings.TrimSpace(e.Name) - if e.Name == "" { - return errors.New(errs.ErrNameRequired) - } - return nil -} - -// PushHistory 推送日志/历史实体 -type PushHistory struct { - ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` - EventKey string `json:"event_key" gorm:"size:80;not null;index"` - Channel string `json:"channel" gorm:"size:50;not null;index"` - Target string `json:"target" gorm:"size:255;not null"` - Title string `json:"title" gorm:"size:255;not null"` - Content string `json:"content" gorm:"type:text;not null"` - Level string `json:"level" gorm:"size:20;not null;default:'INFO'"` - Status string `json:"status" gorm:"size:20;not null;index"` - ErrorMsg string `json:"error_msg" gorm:"type:text"` - Payload string `json:"payload" gorm:"type:text"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` -} - -// TableName 指定 GORM 表名 -func (PushHistory) TableName() string { - return "w_push_histories" -} - -// BindRequest is the user bind body. -type BindRequest struct { - ChannelID string `json:"channel_id"` - Code string `json:"code"` -} - -// BindingDTO is a user-facing binding row. -type BindingDTO struct { - ID uint64 `json:"id,string"` - UserID uint64 `json:"user_id,string"` - ChannelID uint64 `json:"channel_id,string"` - ChannelName string `json:"channel_name"` - ChannelType string `json:"channel_type"` - PlatformUserID string `json:"platform_user_id"` - CreatedAt time.Time `json:"created_at"` -} - -// PublicChannelDTO is an enabled channel a user can bind to. -type PublicChannelDTO struct { - ID uint64 `json:"id,string"` - Name string `json:"name"` - Type string `json:"type"` -} - -// PushNotificationEvent defines the payload for eventbus notification trigger. -type PushNotificationEvent struct { - UserID uint64 `json:"user_id"` - Channel string `json:"channel"` - Title string `json:"title"` - Content string `json:"content"` - Metadata map[string]any `json:"metadata,omitempty"` -} diff --git a/backend/plugins/domain/message_gateway/channels/qq/adapter.go b/backend/plugins/domain/msg_gateway/channels/qq/adapter.go similarity index 85% rename from backend/plugins/domain/message_gateway/channels/qq/adapter.go rename to backend/plugins/domain/msg_gateway/channels/qq/adapter.go index 828f7d4d..a72899ab 100644 --- a/backend/plugins/domain/message_gateway/channels/qq/adapter.go +++ b/backend/plugins/domain/msg_gateway/channels/qq/adapter.go @@ -7,8 +7,9 @@ package qq import ( "Wavelet/pkg/logger" "Wavelet/pkg/util" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/service" "context" "fmt" "strings" @@ -33,7 +34,7 @@ type qqEvent struct { // Adapter is an official QQ Bot C2C channel. type Adapter struct { - cfg model.ChannelConfig + cfg do.ChannelConfig onInbound service.Handler api openapi.OpenAPI tokenSrc oauth2.TokenSource @@ -43,7 +44,7 @@ type Adapter struct { } // New constructs a QQ adapter. -func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, error) { +func New(cfg do.ChannelConfig, onInbound service.Handler) (service.Channel, error) { if strings.TrimSpace(cfg.Credentials["app_id"]) == "" || strings.TrimSpace(cfg.Credentials["app_secret"]) == "" { return nil, fmt.Errorf("qq: app_id and app_secret are required") } @@ -51,11 +52,11 @@ func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, e } // Type returns qq. -func (a *Adapter) Type() string { return model.ChannelTypeQQ } +func (a *Adapter) Type() string { return consts.ChannelTypeQQ } // Capabilities reports C2C text/media support. -func (a *Adapter) Capabilities() model.Capability { - return model.Capability{Text: true, Image: true, File: true, Reply: true} +func (a *Adapter) Capabilities() do.Capability { + return do.Capability{Text: true, Image: true, File: true, Reply: true} } // Connect starts the official WebSocket session (C2C intent). @@ -128,7 +129,7 @@ func (a *Adapter) Disconnect(_ context.Context) error { } // Send posts a C2C text reply. -func (a *Adapter) Send(ctx context.Context, to model.Recipient, msg model.OutboundMessage) error { +func (a *Adapter) Send(ctx context.Context, to do.Recipient, msg do.OutboundMessage) error { a.mu.Lock() api := a.api a.mu.Unlock() @@ -152,9 +153,9 @@ func (a *Adapter) handleEvent(ctx context.Context, ev qqEvent) { if disconnected || a.onInbound == nil { return } - _ = a.onInbound(ctx, model.InboundMessage{ + _ = a.onInbound(ctx, do.InboundMessage{ ChannelID: a.cfg.ID, - ChannelType: model.ChannelTypeQQ, + ChannelType: consts.ChannelTypeQQ, PlatformUserID: ev.UserID, ChatID: ev.UserID, MessageID: ev.MessageID, diff --git a/backend/plugins/domain/message_gateway/channels/qq/adapter_test.go b/backend/plugins/domain/msg_gateway/channels/qq/adapter_test.go similarity index 69% rename from backend/plugins/domain/message_gateway/channels/qq/adapter_test.go rename to backend/plugins/domain/msg_gateway/channels/qq/adapter_test.go index fe4b9f62..2d265a6a 100644 --- a/backend/plugins/domain/message_gateway/channels/qq/adapter_test.go +++ b/backend/plugins/domain/msg_gateway/channels/qq/adapter_test.go @@ -4,14 +4,14 @@ package qq import ( - "Wavelet/plugins/domain/message_gateway/model" + "Wavelet/plugins/domain/msg_gateway/model/do" "context" "testing" ) func TestHandleEvent_DropsNonC2C(t *testing.T) { var got int - a := &Adapter{onInbound: func(ctx context.Context, msg model.InboundMessage) error { + a := &Adapter{onInbound: func(_ context.Context, _ do.InboundMessage) error { got++ return nil }} @@ -22,8 +22,8 @@ func TestHandleEvent_DropsNonC2C(t *testing.T) { } func TestHandleEvent_C2CText(t *testing.T) { - var got model.InboundMessage - a := &Adapter{cfg: model.ChannelConfig{ID: 3}, onInbound: func(ctx context.Context, msg model.InboundMessage) error { + var got do.InboundMessage + a := &Adapter{cfg: do.ChannelConfig{ID: 3}, onInbound: func(_ context.Context, msg do.InboundMessage) error { got = msg return nil }} @@ -34,7 +34,7 @@ func TestHandleEvent_C2CText(t *testing.T) { } func TestNew_RequiresCreds(t *testing.T) { - _, err := New(model.ChannelConfig{}, nil) + _, err := New(do.ChannelConfig{}, nil) if err == nil { t.Fatal("expected error") } diff --git a/backend/plugins/domain/message_gateway/channels/telegram/adapter.go b/backend/plugins/domain/msg_gateway/channels/telegram/adapter.go similarity index 80% rename from backend/plugins/domain/message_gateway/channels/telegram/adapter.go rename to backend/plugins/domain/msg_gateway/channels/telegram/adapter.go index eb044e87..e4bf9a6a 100644 --- a/backend/plugins/domain/message_gateway/channels/telegram/adapter.go +++ b/backend/plugins/domain/msg_gateway/channels/telegram/adapter.go @@ -7,8 +7,9 @@ package telegram import ( "Wavelet/pkg/logger" "Wavelet/pkg/util" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/service" "context" "fmt" "os" @@ -22,13 +23,13 @@ import ( // Adapter is a Telegram private-chat channel. type Adapter struct { - cfg model.ChannelConfig + cfg do.ChannelConfig onInbound service.Handler bot *tele.Bot } // New constructs a Telegram adapter. Call service.Register from the runner. -func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, error) { +func New(cfg do.ChannelConfig, onInbound service.Handler) (service.Channel, error) { if strings.TrimSpace(cfg.Credentials["bot_token"]) == "" { return nil, fmt.Errorf("telegram: bot_token is required") } @@ -36,11 +37,11 @@ func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, e } // Type returns telegram. -func (a *Adapter) Type() string { return model.ChannelTypeTelegram } +func (a *Adapter) Type() string { return consts.ChannelTypeTelegram } // Capabilities reports private-chat media support. -func (a *Adapter) Capabilities() model.Capability { - return model.Capability{Text: true, Image: true, File: true, Reply: true} +func (a *Adapter) Capabilities() do.Capability { + return do.Capability{Text: true, Image: true, File: true, Reply: true} } // longPollWindow is how long Telegram may hold a getUpdates call open before @@ -50,7 +51,7 @@ func (a *Adapter) Capabilities() model.Capability { const longPollWindow = 10 * time.Second // buildTeleSettings assembles the telebot settings. -func buildTeleSettings(cfg model.ChannelConfig) tele.Settings { +func buildTeleSettings(cfg do.ChannelConfig) tele.Settings { pref := tele.Settings{ Token: cfg.Credentials["bot_token"], Poller: &tele.LongPoller{Timeout: longPollWindow}, @@ -99,7 +100,7 @@ func (a *Adapter) Disconnect(_ context.Context) error { } // Send replies to a private chat. -func (a *Adapter) Send(_ context.Context, to model.Recipient, msg model.OutboundMessage) error { +func (a *Adapter) Send(_ context.Context, to do.Recipient, msg do.OutboundMessage) error { if a.bot == nil { return fmt.Errorf("telegram: not connected") } @@ -118,9 +119,9 @@ func (a *Adapter) handleTeleMessage(ctx context.Context, m *tele.Message) { if a.onInbound == nil { return } - msg := model.InboundMessage{ + msg := do.InboundMessage{ ChannelID: a.cfg.ID, - ChannelType: model.ChannelTypeTelegram, + ChannelType: consts.ChannelTypeTelegram, PlatformUserID: strconv.FormatInt(m.Sender.ID, 10), ChatID: strconv.FormatInt(m.Chat.ID, 10), MessageID: strconv.Itoa(m.ID), @@ -146,7 +147,7 @@ func (a *Adapter) handleTeleMessage(ctx context.Context, m *tele.Message) { // downloadMedia fetches message media into a scratch directory, returned so the // caller can remove it once the inbound handler no longer needs the paths. // An empty dir means nothing was downloaded. -func (a *Adapter) downloadMedia(m *tele.Message) (string, []model.Attachment) { +func (a *Adapter) downloadMedia(m *tele.Message) (string, []do.Attachment) { var files []*tele.File var names []string if m.Photo != nil { @@ -166,16 +167,16 @@ func (a *Adapter) downloadMedia(m *tele.Message) (string, []model.Attachment) { } dir, err := os.MkdirTemp("", "wg-tg-*") if err != nil { - return "", []model.Attachment{{Error: err.Error()}} + return "", []do.Attachment{{Error: err.Error()}} } - out := make([]model.Attachment, 0, len(files)) + out := make([]do.Attachment, 0, len(files)) for i, f := range files { path := filepath.Join(dir, names[i]) if err := a.bot.Download(f, path); err != nil { - out = append(out, model.Attachment{FileName: names[i], Error: err.Error()}) + out = append(out, do.Attachment{FileName: names[i], Error: err.Error()}) continue } - out = append(out, model.Attachment{Path: path, FileName: names[i]}) + out = append(out, do.Attachment{Path: path, FileName: names[i]}) } return dir, out } diff --git a/backend/plugins/domain/message_gateway/channels/telegram/adapter_test.go b/backend/plugins/domain/msg_gateway/channels/telegram/adapter_test.go similarity index 82% rename from backend/plugins/domain/message_gateway/channels/telegram/adapter_test.go rename to backend/plugins/domain/msg_gateway/channels/telegram/adapter_test.go index cd700e05..c2c75ef8 100644 --- a/backend/plugins/domain/message_gateway/channels/telegram/adapter_test.go +++ b/backend/plugins/domain/msg_gateway/channels/telegram/adapter_test.go @@ -4,7 +4,7 @@ package telegram import ( - "Wavelet/plugins/domain/message_gateway/model" + "Wavelet/plugins/domain/msg_gateway/model/do" "context" "testing" "time" @@ -16,7 +16,7 @@ import ( // telebot 以 int(timeout/time.Second) 下发给 getUpdates。写成裸整数会被解释为 // 纳秒,令 timeout=0,长轮询退化为对 Bot API 的空转轮询。 func TestBuildTeleSettingsLongPollWindow(t *testing.T) { - pref := buildTeleSettings(model.ChannelConfig{ + pref := buildTeleSettings(do.ChannelConfig{ Credentials: map[string]string{"bot_token": "token"}, Extra: map[string]string{"base_url": "https://tg.example.com/api/"}, }) @@ -35,7 +35,7 @@ func TestBuildTeleSettingsLongPollWindow(t *testing.T) { func TestHandleUpdate_DropsGroups(t *testing.T) { var got int - a := &Adapter{onInbound: func(ctx context.Context, msg model.InboundMessage) error { + a := &Adapter{onInbound: func(_ context.Context, _ do.InboundMessage) error { got++ return nil }} @@ -51,10 +51,10 @@ func TestHandleUpdate_DropsGroups(t *testing.T) { } func TestHandleUpdate_PrivateText(t *testing.T) { - var got model.InboundMessage + var got do.InboundMessage a := &Adapter{ - cfg: model.ChannelConfig{ID: 7, Type: "telegram"}, - onInbound: func(ctx context.Context, msg model.InboundMessage) error { + cfg: do.ChannelConfig{ID: 7, Type: "telegram"}, + onInbound: func(_ context.Context, msg do.InboundMessage) error { got = msg return nil }, @@ -71,7 +71,7 @@ func TestHandleUpdate_PrivateText(t *testing.T) { } func TestNew_RequiresToken(t *testing.T) { - _, err := New(model.ChannelConfig{}, nil) + _, err := New(do.ChannelConfig{}, nil) if err == nil { t.Fatal("expected error") } diff --git a/backend/plugins/domain/message_gateway/errs/errs.go b/backend/plugins/domain/msg_gateway/consts/consts.go similarity index 65% rename from backend/plugins/domain/message_gateway/errs/errs.go rename to backend/plugins/domain/msg_gateway/consts/consts.go index 6d4c1538..a6bc3b23 100644 --- a/backend/plugins/domain/message_gateway/errs/errs.go +++ b/backend/plugins/domain/msg_gateway/consts/consts.go @@ -1,12 +1,69 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -// Package errs defines error sentinels and user-facing error message constants -// for the message_gateway plugin. -package errs +// Package consts defines constants, sentinel errors, and user-facing error messages +// for the msg_gateway plugin. +package consts import "errors" +// Channel type and scope constants. +const ( + ChannelTypeTelegram = "telegram" + ChannelTypeQQ = "qq" + MessageChannelTypeTelegram = "telegram" + MessageChannelTypeQQ = "qq" + MessageOwnerScopeSystem = "system" + + TypeCustom = "custom" + TypeEmail = "email" + TypeTelegram = "telegram" + ChannelCustom = "custom" + ChannelEmail = "email" + ChannelLark = "lark" + ChannelDingTalk = "dingtalk" + ChannelTelegram = "telegram" + ChannelBark = "bark" + ChannelDiscord = "discord" + ChannelSlack = "slack" + ChannelPushover = "pushover" + + DefaultLevelInfo = "INFO" + KeyTitle = "title" + KeyContent = "content" + KeyLevel = "level" + + // KeyURL represents the URL field key. + KeyURL = "url" + // KeyToken represents the Token field key. + KeyToken = "token" + // KeyOther represents the Other field key. + KeyOther = "other" + + // TypeText represents standard text input type. + TypeText = "text" + // TypePassword represents password input type. + TypePassword = "password" + // TypeTextarea represents textarea input type. + TypeTextarea = "textarea" +) + +// Task and Schedule identifier constants. +const ( + TaskPushNotification = "msg_gateway:push_notification" + TaskCleanupPairingCodes = "msg_gateway:cleanup_pairing_codes" + TaskDispatchBotMsg = "msg_gateway:dispatch_bot_msg" + TaskTypeDispatchBotMsg = "dispatch_bot_msg" + SendNotificationTask = "push:send" + TaskTypeSendNotification = "send_notification" +) + +// Pairing code constants. +const ( + CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" + CodeLength = 8 +) + // Sentinel errors. var ( ErrCodeInvalid = errors.New("invalid or expired pairing code") @@ -17,11 +74,11 @@ var ( ErrChannelIDRequired = errors.New("channel_id is required") ErrChannelDisabled = errors.New("channel is not enabled") - // ErrRecordNotFound maps GORM's missing-row sentinel at the repository boundary so + // ErrRecordNotFound maps GORM's missing-row sentinel at the DAO boundary so // upper layers never import gorm. Its text matches gorm.ErrRecordNotFound verbatim. ErrRecordNotFound = errors.New("record not found") - // ErrUnsupportedUserLookupField rejects a column name that the repository is not + // ErrUnsupportedUserLookupField rejects a column name that the DAO is not // allowed to interpolate into a WHERE clause. ErrUnsupportedUserLookupField = errors.New("unsupported user lookup field") ) diff --git a/backend/plugins/domain/message_gateway/handler/admin.go b/backend/plugins/domain/msg_gateway/controller/admin.go similarity index 87% rename from backend/plugins/domain/message_gateway/handler/admin.go rename to backend/plugins/domain/msg_gateway/controller/admin.go index f90ccacd..87b76d1b 100644 --- a/backend/plugins/domain/message_gateway/handler/admin.go +++ b/backend/plugins/domain/msg_gateway/controller/admin.go @@ -1,12 +1,12 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package handler +package controller import ( "Wavelet/pkg/response" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/service" "net/http" "strconv" @@ -19,7 +19,7 @@ import ( // @Tags admin-message-gateway // @Produce json // @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.Definition} +// @Success 200 {object} response.Any{data=[]do.Definition} // @Router /api/v1/admin/message-gateway/channels/definitions [get] func ListAdminChannelDefinitions(c *gin.Context) { c.JSON(http.StatusOK, response.OK(service.ListDefinitions())) @@ -31,7 +31,7 @@ func ListAdminChannelDefinitions(c *gin.Context) { // @Tags admin-message-gateway // @Produce json // @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.ChannelDTO} +// @Success 200 {object} response.Any{data=[]do.ChannelDTO} // @Router /api/v1/admin/message-gateway/channels [get] func ListAdminChannels(c *gin.Context) { rows, err := service.ListChannels(c.Request.Context()) @@ -45,14 +45,14 @@ func ListAdminChannels(c *gin.Context) { func parseAdminChannelID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - response.AbortBadRequest(c, errs.ErrInvalidChannelID) + response.AbortBadRequest(c, consts.ErrInvalidChannelID) return 0, false } return id, true } func handleAdminChannelError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) { - if err.Error() == errs.ErrChannelNotFound { + if err.Error() == consts.ErrChannelNotFound { response.AbortNotFound(c, err.Error()) return } @@ -66,8 +66,8 @@ func handleAdminChannelError(c *gin.Context, err error, fallback func(c *gin.Con // @Accept json // @Produce json // @Security SessionCookie -// @Param request body model.CreateChannelRequest true "create body" -// @Success 200 {object} response.Any{data=model.ChannelDTO} +// @Param request body do.CreateChannelRequest true "create body" +// @Success 200 {object} response.Any{data=do.ChannelDTO} // @Failure 400 {object} response.Any // @Router /api/v1/admin/message-gateway/channels [post] func CreateAdminChannel(c *gin.Context) { @@ -82,8 +82,8 @@ func CreateAdminChannel(c *gin.Context) { // @Produce json // @Security SessionCookie // @Param id path int true "channel id" -// @Param request body model.UpdateChannelRequest true "update body" -// @Success 200 {object} response.Any{data=model.ChannelDTO} +// @Param request body do.UpdateChannelRequest true "update body" +// @Success 200 {object} response.Any{data=do.ChannelDTO} // @Failure 400 {object} response.Any // @Failure 404 {object} response.Any // @Router /api/v1/admin/message-gateway/channels/{id} [patch] diff --git a/backend/plugins/domain/message_gateway/handler/push_channel.go b/backend/plugins/domain/msg_gateway/controller/push_channel.go similarity index 81% rename from backend/plugins/domain/message_gateway/handler/push_channel.go rename to backend/plugins/domain/msg_gateway/controller/push_channel.go index c3c07a72..8919560b 100644 --- a/backend/plugins/domain/message_gateway/handler/push_channel.go +++ b/backend/plugins/domain/msg_gateway/controller/push_channel.go @@ -1,13 +1,13 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package handler +package controller import ( "Wavelet/pkg/response" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/service" "errors" "net/http" "strconv" @@ -24,7 +24,7 @@ import ( // @Success 200 {object} response.Any "通道配置定义列表" // @Router /api/v1/admin/push/channels/definitions [get] func ListPushChannelDefinitions(c *gin.Context) { - c.JSON(http.StatusOK, response.OK(model.ListPushDefinitions())) + c.JSON(http.StatusOK, response.OK(do.ListPushDefinitions())) } // ListPushChannels 获取消息通道列表 @@ -33,7 +33,7 @@ func ListPushChannelDefinitions(c *gin.Context) { // @Tags admin-push // @Produce json // @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.PushChannel} "消息通道列表" +// @Success 200 {object} response.Any{data=[]entity.PushChannel} "消息通道列表" // @Router /api/v1/admin/push/channels [get] func ListPushChannels(c *gin.Context) { channels, err := service.ListPushChannels(c.Request.Context()) @@ -48,7 +48,7 @@ func ListPushChannels(c *gin.Context) { func parsePushChannelID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - response.AbortBadRequest(c, errs.ErrInvalidChannelID) + response.AbortBadRequest(c, consts.ErrInvalidChannelID) return 0, false } return id, true @@ -56,8 +56,8 @@ func parsePushChannelID(c *gin.Context) (uint64, bool) { // handlePushChannelNotFoundError maps a missing channel row to 404, others to fallback. func handlePushChannelNotFoundError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) { - if errors.Is(err, errs.ErrRecordNotFound) { - response.AbortNotFound(c, errs.ErrChannelNotFound) + if errors.Is(err, consts.ErrRecordNotFound) { + response.AbortNotFound(c, consts.ErrChannelNotFound) return } fallback(c, err.Error()) @@ -70,8 +70,8 @@ func handlePushChannelNotFoundError(c *gin.Context, err error, fallback func(c * // @Accept json // @Produce json // @Security SessionCookie -// @Param request body model.CreatePushChannelRequest true "创建参数" -// @Success 200 {object} response.Any{data=model.PushChannel} "创建成功" +// @Param request body do.CreatePushChannelRequest true "创建参数" +// @Success 200 {object} response.Any{data=entity.PushChannel} "创建成功" // @Router /api/v1/admin/push/channels [post] func CreatePushChannel(c *gin.Context) { handleJSONRequest(c, service.CreatePushChannel) @@ -85,8 +85,8 @@ func CreatePushChannel(c *gin.Context) { // @Produce json // @Security SessionCookie // @Param id path uint64 true "通道ID" -// @Param request body model.UpdatePushChannelRequest true "更新参数" -// @Success 200 {object} response.Any{data=model.PushChannel} "更新成功" +// @Param request body do.UpdatePushChannelRequest true "更新参数" +// @Success 200 {object} response.Any{data=entity.PushChannel} "更新成功" // @Router /api/v1/admin/push/channels/{id} [put] func UpdatePushChannel(c *gin.Context) { handleEntityUpdate(c, parsePushChannelID, service.UpdatePushChannel, func(c *gin.Context, err error) { @@ -123,11 +123,11 @@ func DeletePushChannel(c *gin.Context) { // @Accept json // @Produce json // @Security SessionCookie -// @Param request body model.TestPushChannelRequest true "测试参数" +// @Param request body do.TestPushChannelRequest true "测试参数" // @Success 200 {object} response.Any "测试触发成功" // @Router /api/v1/admin/push/channels/test [post] func TestPushChannel(c *gin.Context) { - var req model.TestPushChannelRequest + var req do.TestPushChannelRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortBadRequest(c, err.Error()) return diff --git a/backend/plugins/domain/message_gateway/handler/push_event.go b/backend/plugins/domain/msg_gateway/controller/push_event.go similarity index 88% rename from backend/plugins/domain/message_gateway/handler/push_event.go rename to backend/plugins/domain/msg_gateway/controller/push_event.go index 04c6e717..05b3fd40 100644 --- a/backend/plugins/domain/message_gateway/handler/push_event.go +++ b/backend/plugins/domain/msg_gateway/controller/push_event.go @@ -1,13 +1,13 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package handler +package controller import ( "Wavelet/pkg/response" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/service" "errors" "net/http" "strconv" @@ -21,7 +21,7 @@ import ( // @Tags admin-push // @Produce json // @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.PushEvent} "通知事件列表" +// @Success 200 {object} response.Any{data=[]entity.PushEvent} "通知事件列表" // @Router /api/v1/admin/push/events [get] func ListPushEvents(c *gin.Context) { ctx := c.Request.Context() @@ -49,7 +49,7 @@ func ListBuiltInPushEvents(c *gin.Context) { func parsePushEventID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - response.AbortBadRequest(c, errs.ErrInvalidEventID) + response.AbortBadRequest(c, consts.ErrInvalidEventID) return 0, false } return id, true @@ -57,8 +57,8 @@ func parsePushEventID(c *gin.Context) (uint64, bool) { // handlePushEventNotFoundError maps a missing event row to 404, others to fallback. func handlePushEventNotFoundError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) { - if errors.Is(err, errs.ErrRecordNotFound) { - response.AbortNotFound(c, errs.ErrEventNotFound) + if errors.Is(err, consts.ErrRecordNotFound) { + response.AbortNotFound(c, consts.ErrEventNotFound) return } fallback(c, err.Error()) @@ -71,8 +71,8 @@ func handlePushEventNotFoundError(c *gin.Context, err error, fallback func(c *gi // @Accept json // @Produce json // @Security SessionCookie -// @Param request body model.CreatePushEventRequest true "创建参数" -// @Success 200 {object} response.Any{data=model.PushEvent} "创建成功" +// @Param request body do.CreatePushEventRequest true "创建参数" +// @Success 200 {object} response.Any{data=entity.PushEvent} "创建成功" // @Router /api/v1/admin/push/events [post] func CreatePushEvent(c *gin.Context) { handleJSONRequest(c, service.CreatePushEvent) @@ -108,7 +108,7 @@ func DeletePushEvent(c *gin.Context) { // @Produce json // @Security SessionCookie // @Param id path int true "事件 ID" -// @Param request body model.UpdatePushEventRequest true "更新参数" +// @Param request body do.UpdatePushEventRequest true "更新参数" // @Success 200 {object} response.Any{data=string} "修改成功" // @Router /api/v1/admin/push/events/{id} [put] func UpdatePushEvent(c *gin.Context) { @@ -117,7 +117,7 @@ func UpdatePushEvent(c *gin.Context) { return } - var req model.UpdatePushEventRequest + var req do.UpdatePushEventRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortBadRequest(c, err.Error()) return @@ -175,8 +175,9 @@ func ListPushHistories(c *gin.Context) { pageSize = 20 } - total, results, err := service.ListPushHistories(c.Request.Context(), model.PushHistoryListFilter{ + total, results, err := service.ListPushHistories(c.Request.Context(), do.PushHistoryListFilter{ EventKey: c.Query("event_key"), + Channel: c.Query("channel"), Status: c.Query("status"), Page: page, PageSize: pageSize, @@ -199,11 +200,11 @@ func ListPushHistories(c *gin.Context) { // @Accept json // @Produce json // @Security SessionCookie -// @Param request body model.TestPushRequest true "测试请求体" +// @Param request body do.TestPushRequest true "测试请求体" // @Success 200 {object} response.Any{data=string} "测试成功" // @Router /api/v1/admin/push/test [post] func TestPush(c *gin.Context) { - var req model.TestPushRequest + var req do.TestPushRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortBadRequest(c, err.Error()) return diff --git a/backend/plugins/domain/message_gateway/handler/router.go b/backend/plugins/domain/msg_gateway/controller/router.go similarity index 96% rename from backend/plugins/domain/message_gateway/handler/router.go rename to backend/plugins/domain/msg_gateway/controller/router.go index 837505e6..cc8f1ec7 100644 --- a/backend/plugins/domain/message_gateway/handler/router.go +++ b/backend/plugins/domain/msg_gateway/controller/router.go @@ -1,7 +1,8 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package handler +// Package controller provides HTTP endpoints for msg_gateway. +package controller import ( "Wavelet/core/extpoints" diff --git a/backend/plugins/domain/message_gateway/handler/handlers.go b/backend/plugins/domain/msg_gateway/controller/user.go similarity index 83% rename from backend/plugins/domain/message_gateway/handler/handlers.go rename to backend/plugins/domain/msg_gateway/controller/user.go index 877ade86..2d99a26e 100644 --- a/backend/plugins/domain/message_gateway/handler/handlers.go +++ b/backend/plugins/domain/msg_gateway/controller/user.go @@ -1,16 +1,15 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -// Package handler provides HTTP endpoints for message_gateway. -package handler +package controller import ( "Wavelet/core/contracts" "Wavelet/pkg/ginutil" "Wavelet/pkg/response" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/service" "context" "errors" "net/http" @@ -70,12 +69,12 @@ func handleEntityUpdate[Req any, Res any]( // @Tags message-gateway // @Produce json // @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.PublicChannelDTO} +// @Success 200 {object} response.Any{data=[]do.PublicChannelDTO} // @Failure 401 {object} response.Any // @Router /api/v1/message-gateway/channels [get] func ListChannels(c *gin.Context) { if user, ok := currentUser(c); !ok || user == nil { - response.AbortUnauthorized(c, errs.ErrLoginRequired) + response.AbortUnauthorized(c, consts.ErrLoginRequired) return } rows, err := service.ListEnabledPublicChannels(c.Request.Context()) @@ -92,13 +91,13 @@ func ListChannels(c *gin.Context) { // @Tags message-gateway // @Produce json // @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.BindingDTO} +// @Success 200 {object} response.Any{data=[]do.BindingDTO} // @Failure 401 {object} response.Any // @Router /api/v1/message-gateway/bindings [get] func ListBindings(c *gin.Context) { user, ok := currentUser(c) if !ok || user == nil { - response.AbortUnauthorized(c, errs.ErrLoginRequired) + response.AbortUnauthorized(c, consts.ErrLoginRequired) return } rows, err := service.ListUserBindings(c.Request.Context(), user.ID) @@ -116,25 +115,25 @@ func ListBindings(c *gin.Context) { // @Accept json // @Produce json // @Security SessionCookie -// @Param request body model.BindRequest true "bind body" -// @Success 200 {object} response.Any{data=model.BindingDTO} +// @Param request body do.BindRequest true "bind body" +// @Success 200 {object} response.Any{data=do.BindingDTO} // @Failure 400 {object} response.Any // @Failure 409 {object} response.Any // @Router /api/v1/message-gateway/bindings [post] func BindBinding(c *gin.Context) { user, ok := currentUser(c) if !ok || user == nil { - response.AbortUnauthorized(c, errs.ErrLoginRequired) + response.AbortUnauthorized(c, consts.ErrLoginRequired) return } - var req model.BindRequest + var req do.BindRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortBadRequest(c, err.Error()) return } dto, err := service.BindChannel(c.Request.Context(), user.ID, req) if err != nil { - if errors.Is(err, errs.ErrPlatformAlreadyBound) { + if errors.Is(err, consts.ErrPlatformAlreadyBound) { response.AbortConflict(c, err.Error()) return } @@ -158,20 +157,20 @@ func BindBinding(c *gin.Context) { func UnbindBinding(c *gin.Context) { user, ok := currentUser(c) if !ok || user == nil { - response.AbortUnauthorized(c, errs.ErrLoginRequired) + response.AbortUnauthorized(c, consts.ErrLoginRequired) return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - response.AbortBadRequest(c, errs.ErrInvalidBindingID) + response.AbortBadRequest(c, consts.ErrInvalidBindingID) return } if err := service.UnbindChannel(c.Request.Context(), user.ID, id); err != nil { - if errors.Is(err, errs.ErrBindingNotFound) { + if errors.Is(err, consts.ErrBindingNotFound) { response.AbortNotFound(c, err.Error()) return } - if errors.Is(err, errs.ErrBindingForbidden) { + if errors.Is(err, consts.ErrBindingForbidden) { response.AbortForbidden(c, err.Error()) return } diff --git a/backend/plugins/domain/message_gateway/repository/repository.go b/backend/plugins/domain/msg_gateway/dao/dao.go similarity index 69% rename from backend/plugins/domain/message_gateway/repository/repository.go rename to backend/plugins/domain/msg_gateway/dao/dao.go index 52a78eab..977cc583 100644 --- a/backend/plugins/domain/message_gateway/repository/repository.go +++ b/backend/plugins/domain/msg_gateway/dao/dao.go @@ -1,15 +1,15 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -// Package repository provides data persistence for the message_gateway plugin. -package repository +// Package dao provides database persistence and caching for the msg_gateway plugin. +package dao import ( "Wavelet/core" "Wavelet/core/contracts" "Wavelet/pkg/idgen" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/entity" "context" "errors" "sync" @@ -53,16 +53,16 @@ func GetDB(ctx context.Context) *gorm.DB { } // mapNotFound translates GORM's missing-row sentinel into the plugin-level -// errs.ErrRecordNotFound so the service and handler layers stay free of gorm imports. +// consts.ErrRecordNotFound so the service and controller layers stay free of gorm imports. func mapNotFound(err error) error { if errors.Is(err, gorm.ErrRecordNotFound) { - return errs.ErrRecordNotFound + return consts.ErrRecordNotFound } return err } // CreateMessageChannel inserts a channel row. -func CreateMessageChannel(ctx context.Context, ch *model.MessageChannel) error { +func CreateMessageChannel(ctx context.Context, ch *entity.MessageChannel) error { if ch.ID == 0 { ch.ID = idgen.NextUint64ID() } @@ -70,13 +70,13 @@ func CreateMessageChannel(ctx context.Context, ch *model.MessageChannel) error { } // UpdateMessageChannel saves a channel row. -func UpdateMessageChannel(ctx context.Context, ch *model.MessageChannel) error { +func UpdateMessageChannel(ctx context.Context, ch *entity.MessageChannel) error { return GetDB(ctx).Save(ch).Error } // GetMessageChannel loads a channel by id. -func GetMessageChannel(ctx context.Context, id uint64) (*model.MessageChannel, error) { - var ch model.MessageChannel +func GetMessageChannel(ctx context.Context, id uint64) (*entity.MessageChannel, error) { + var ch entity.MessageChannel if err := GetDB(ctx).Where("id = ?", id).First(&ch).Error; err != nil { return nil, mapNotFound(err) } @@ -84,8 +84,8 @@ func GetMessageChannel(ctx context.Context, id uint64) (*model.MessageChannel, e } // ListMessageChannels returns all channels newest first. -func ListMessageChannels(ctx context.Context) ([]model.MessageChannel, error) { - var rows []model.MessageChannel +func ListMessageChannels(ctx context.Context) ([]entity.MessageChannel, error) { + var rows []entity.MessageChannel if err := GetDB(ctx).Order("id DESC").Find(&rows).Error; err != nil { return nil, err } @@ -95,18 +95,18 @@ func ListMessageChannels(ctx context.Context) ([]model.MessageChannel, error) { // DeleteMessageChannel removes pairings, bindings, then the channel. func DeleteMessageChannel(ctx context.Context, id uint64) error { return GetDB(ctx).Transaction(func(tx *gorm.DB) error { - if err := tx.Where("channel_id = ?", id).Delete(&model.MessagePairingCode{}).Error; err != nil { + if err := tx.Where("channel_id = ?", id).Delete(&entity.MessagePairingCode{}).Error; err != nil { return err } - if err := tx.Where("channel_id = ?", id).Delete(&model.MessageBinding{}).Error; err != nil { + if err := tx.Where("channel_id = ?", id).Delete(&entity.MessageBinding{}).Error; err != nil { return err } - return tx.Delete(&model.MessageChannel{}, id).Error + return tx.Delete(&entity.MessageChannel{}, id).Error }) } // CreateMessageBinding inserts a binding. -func CreateMessageBinding(ctx context.Context, b *model.MessageBinding) error { +func CreateMessageBinding(ctx context.Context, b *entity.MessageBinding) error { if b.ID == 0 { b.ID = idgen.NextUint64ID() } @@ -114,8 +114,8 @@ func CreateMessageBinding(ctx context.Context, b *model.MessageBinding) error { } // GetBindingByChannelPlatform finds a binding for a platform user on a channel. -func GetBindingByChannelPlatform(ctx context.Context, channelID uint64, platformUserID string) (*model.MessageBinding, error) { - var b model.MessageBinding +func GetBindingByChannelPlatform(ctx context.Context, channelID uint64, platformUserID string) (*entity.MessageBinding, error) { + var b entity.MessageBinding err := GetDB(ctx).Where("channel_id = ? AND platform_user_id = ?", channelID, platformUserID).First(&b).Error if err != nil { return nil, mapNotFound(err) @@ -124,8 +124,8 @@ func GetBindingByChannelPlatform(ctx context.Context, channelID uint64, platform } // ListBindingsByUser lists bindings for a Wavelet user. -func ListBindingsByUser(ctx context.Context, userID uint64) ([]model.MessageBinding, error) { - var rows []model.MessageBinding +func ListBindingsByUser(ctx context.Context, userID uint64) ([]entity.MessageBinding, error) { + var rows []entity.MessageBinding if err := GetDB(ctx).Where("user_id = ?", userID).Order("id DESC").Find(&rows).Error; err != nil { return nil, err } @@ -133,8 +133,8 @@ func ListBindingsByUser(ctx context.Context, userID uint64) ([]model.MessageBind } // ListBindingsByChannel lists bindings on one messaging channel. -func ListBindingsByChannel(ctx context.Context, channelID uint64) ([]model.MessageBinding, error) { - var rows []model.MessageBinding +func ListBindingsByChannel(ctx context.Context, channelID uint64) ([]entity.MessageBinding, error) { + var rows []entity.MessageBinding if err := GetDB(ctx).Where("channel_id = ?", channelID).Order("id DESC").Find(&rows).Error; err != nil { return nil, err } @@ -142,8 +142,8 @@ func ListBindingsByChannel(ctx context.Context, channelID uint64) ([]model.Messa } // GetMessageBinding loads a binding by id. -func GetMessageBinding(ctx context.Context, id uint64) (*model.MessageBinding, error) { - var b model.MessageBinding +func GetMessageBinding(ctx context.Context, id uint64) (*entity.MessageBinding, error) { + var b entity.MessageBinding if err := GetDB(ctx).Where("id = ?", id).First(&b).Error; err != nil { return nil, mapNotFound(err) } @@ -152,12 +152,12 @@ func GetMessageBinding(ctx context.Context, id uint64) (*model.MessageBinding, e // DeleteMessageBinding deletes a binding by id. func DeleteMessageBinding(ctx context.Context, id uint64) error { - return GetDB(ctx).Delete(&model.MessageBinding{}, id).Error + return GetDB(ctx).Delete(&entity.MessageBinding{}, id).Error } // UpsertPairingCode reuses an unexpired code for the same channel+platform user. -func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, code string, expiresAt time.Time) (*model.MessagePairingCode, error) { - var existing model.MessagePairingCode +func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, code string, expiresAt time.Time) (*entity.MessagePairingCode, error) { + var existing entity.MessagePairingCode err := GetDB(ctx). Where("channel_id = ? AND platform_user_id = ? AND expires_at > ?", channelID, platformUserID, time.Now()). First(&existing).Error @@ -167,7 +167,7 @@ func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, co if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } - row := &model.MessagePairingCode{ + row := &entity.MessagePairingCode{ Code: code, ChannelID: channelID, PlatformUserID: platformUserID, @@ -180,8 +180,8 @@ func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, co } // GetPairingCode loads a pairing code by normalized code string. -func GetPairingCode(ctx context.Context, code string) (*model.MessagePairingCode, error) { - var row model.MessagePairingCode +func GetPairingCode(ctx context.Context, code string) (*entity.MessagePairingCode, error) { + var row entity.MessagePairingCode if err := GetDB(ctx).Where("code = ?", code).First(&row).Error; err != nil { return nil, mapNotFound(err) } @@ -190,17 +190,17 @@ func GetPairingCode(ctx context.Context, code string) (*model.MessagePairingCode // DeletePairingCode removes a pairing code. func DeletePairingCode(ctx context.Context, code string) error { - return GetDB(ctx).Where("code = ?", code).Delete(&model.MessagePairingCode{}).Error + return GetDB(ctx).Where("code = ?", code).Delete(&entity.MessagePairingCode{}).Error } // DeleteExpiredPairingCodes removes expired pairing rows. func DeleteExpiredPairingCodes(ctx context.Context) error { - return GetDB(ctx).Where("expires_at <= ?", time.Now()).Delete(&model.MessagePairingCode{}).Error + return GetDB(ctx).Where("expires_at <= ?", time.Now()).Delete(&entity.MessagePairingCode{}).Error } // ListEnabledMessageChannels returns enabled channels. -func ListEnabledMessageChannels(ctx context.Context) ([]model.MessageChannel, error) { - var rows []model.MessageChannel +func ListEnabledMessageChannels(ctx context.Context) ([]entity.MessageChannel, error) { + var rows []entity.MessageChannel if err := GetDB(ctx).Where("enabled = ?", true).Find(&rows).Error; err != nil { return nil, err } diff --git a/backend/plugins/domain/message_gateway/repository/push.go b/backend/plugins/domain/msg_gateway/dao/push.go similarity index 73% rename from backend/plugins/domain/message_gateway/repository/push.go rename to backend/plugins/domain/msg_gateway/dao/push.go index a944c3c9..f6d9e940 100644 --- a/backend/plugins/domain/message_gateway/repository/push.go +++ b/backend/plugins/domain/msg_gateway/dao/push.go @@ -1,13 +1,14 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package repository +package dao import ( "Wavelet/core" "Wavelet/core/contracts" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/model/entity" "context" "errors" "fmt" @@ -48,8 +49,8 @@ func GetCache(ctx context.Context) contracts.CacheService { } // ListPushChannelsRecord returns all push channels ordered by creation time descending. -func ListPushChannelsRecord(ctx context.Context) ([]model.PushChannel, error) { - var channels []model.PushChannel +func ListPushChannelsRecord(ctx context.Context) ([]entity.PushChannel, error) { + var channels []entity.PushChannel if err := GetDB(ctx).Order("created_at DESC").Find(&channels).Error; err != nil { return nil, err } @@ -57,17 +58,17 @@ func ListPushChannelsRecord(ctx context.Context) ([]model.PushChannel, error) { } // GetPushChannelByIDRecord loads a push channel by primary key. -func GetPushChannelByIDRecord(ctx context.Context, id uint64) (model.PushChannel, error) { - var channel model.PushChannel +func GetPushChannelByIDRecord(ctx context.Context, id uint64) (entity.PushChannel, error) { + var channel entity.PushChannel if err := GetDB(ctx).Where("id = ?", id).First(&channel).Error; err != nil { - return model.PushChannel{}, mapNotFound(err) + return entity.PushChannel{}, mapNotFound(err) } return channel, nil } // GetPushChannelByNameRecord loads a push channel by its unique name. -func GetPushChannelByNameRecord(ctx context.Context, name string) (*model.PushChannel, error) { - var channel model.PushChannel +func GetPushChannelByNameRecord(ctx context.Context, name string) (*entity.PushChannel, error) { + var channel entity.PushChannel if err := GetDB(ctx).Where("name = ?", name).First(&channel).Error; err != nil { return nil, mapNotFound(err) } @@ -77,14 +78,14 @@ func GetPushChannelByNameRecord(ctx context.Context, name string) (*model.PushCh // CountPushChannelsByNameRecord returns how many channels share the given name. func CountPushChannelsByNameRecord(ctx context.Context, name string) (int64, error) { var count int64 - if err := GetDB(ctx).Model(&model.PushChannel{}).Where("name = ?", name).Count(&count).Error; err != nil { + if err := GetDB(ctx).Model(&entity.PushChannel{}).Where("name = ?", name).Count(&count).Error; err != nil { return 0, err } return count, nil } // CreatePushChannelRecord persists a new channel and invalidates cache. -func CreatePushChannelRecord(ctx context.Context, channel *model.PushChannel) error { +func CreatePushChannelRecord(ctx context.Context, channel *entity.PushChannel) error { if err := GetDB(ctx).Create(channel).Error; err != nil { return err } @@ -93,7 +94,7 @@ func CreatePushChannelRecord(ctx context.Context, channel *model.PushChannel) er } // SavePushChannelRecord updates a channel and invalidates cache. -func SavePushChannelRecord(ctx context.Context, channel *model.PushChannel) error { +func SavePushChannelRecord(ctx context.Context, channel *entity.PushChannel) error { if err := GetDB(ctx).Save(channel).Error; err != nil { return err } @@ -102,7 +103,7 @@ func SavePushChannelRecord(ctx context.Context, channel *model.PushChannel) erro } // DeletePushChannelRecord removes a channel and invalidates cache. -func DeletePushChannelRecord(ctx context.Context, channel *model.PushChannel) error { +func DeletePushChannelRecord(ctx context.Context, channel *entity.PushChannel) error { if err := GetDB(ctx).Delete(channel).Error; err != nil { return err } @@ -131,8 +132,8 @@ func getCachedOrQuery[T any](ctx context.Context, cacheKey string, ttl time.Dura } // GetActivePushChannelByName loads an enabled push channel, preferring the cache layer. -func GetActivePushChannelByName(ctx context.Context, name string) (*model.PushChannel, error) { - channel, err := getCachedOrQuery(ctx, "push:channel:active:"+name, activePushChannelCacheTTL, func(db *gorm.DB, dest *model.PushChannel) error { +func GetActivePushChannelByName(ctx context.Context, name string) (*entity.PushChannel, error) { + channel, err := getCachedOrQuery(ctx, "push:channel:active:"+name, activePushChannelCacheTTL, func(db *gorm.DB, dest *entity.PushChannel) error { return db.Where("name = ? AND enabled = ?", name, true).First(dest).Error }) if err != nil { @@ -149,8 +150,8 @@ func DeleteActivePushChannelCache(ctx context.Context, name string) { } // ListPushEventsRecord returns all push events ordered by creation time descending. -func ListPushEventsRecord(ctx context.Context) ([]model.PushEvent, error) { - var events []model.PushEvent +func ListPushEventsRecord(ctx context.Context) ([]entity.PushEvent, error) { + var events []entity.PushEvent if err := GetDB(ctx).Order("created_at DESC").Find(&events).Error; err != nil { return nil, err } @@ -158,19 +159,19 @@ func ListPushEventsRecord(ctx context.Context) ([]model.PushEvent, error) { } // GetPushEventByIDRecord loads a push event by primary key. -func GetPushEventByIDRecord(ctx context.Context, id uint64) (model.PushEvent, error) { - var event model.PushEvent +func GetPushEventByIDRecord(ctx context.Context, id uint64) (entity.PushEvent, error) { + var event entity.PushEvent if err := GetDB(ctx).First(&event, id).Error; err != nil { - return model.PushEvent{}, mapNotFound(err) + return entity.PushEvent{}, mapNotFound(err) } return event, nil } // GetPushEventByKeyRecord loads a push event by event key. -func GetPushEventByKeyRecord(ctx context.Context, key string) (model.PushEvent, error) { - var event model.PushEvent +func GetPushEventByKeyRecord(ctx context.Context, key string) (entity.PushEvent, error) { + var event entity.PushEvent if err := GetDB(ctx).Where("event_key = ?", key).First(&event).Error; err != nil { - return model.PushEvent{}, mapNotFound(err) + return entity.PushEvent{}, mapNotFound(err) } return event, nil } @@ -178,14 +179,14 @@ func GetPushEventByKeyRecord(ctx context.Context, key string) (model.PushEvent, // CountPushEventsByKeyRecord returns how many events use the given event key. func CountPushEventsByKeyRecord(ctx context.Context, key string) (int64, error) { var count int64 - if err := GetDB(ctx).Model(&model.PushEvent{}).Where("event_key = ?", key).Count(&count).Error; err != nil { + if err := GetDB(ctx).Model(&entity.PushEvent{}).Where("event_key = ?", key).Count(&count).Error; err != nil { return 0, err } return count, nil } // CreatePushEventRecord persists a new push event and invalidates cache. -func CreatePushEventRecord(ctx context.Context, event *model.PushEvent) error { +func CreatePushEventRecord(ctx context.Context, event *entity.PushEvent) error { if err := GetDB(ctx).Create(event).Error; err != nil { return err } @@ -194,7 +195,7 @@ func CreatePushEventRecord(ctx context.Context, event *model.PushEvent) error { } // SavePushEventRecord updates a push event and invalidates cache. -func SavePushEventRecord(ctx context.Context, event *model.PushEvent) error { +func SavePushEventRecord(ctx context.Context, event *entity.PushEvent) error { if err := GetDB(ctx).Save(event).Error; err != nil { return err } @@ -203,7 +204,7 @@ func SavePushEventRecord(ctx context.Context, event *model.PushEvent) error { } // UpdatePushEventEnabledRecord toggles the enabled flag for a push event. -func UpdatePushEventEnabledRecord(ctx context.Context, event *model.PushEvent, enabled bool) error { +func UpdatePushEventEnabledRecord(ctx context.Context, event *entity.PushEvent, enabled bool) error { event.Enabled = enabled if err := GetDB(ctx).Model(event).Update("enabled", enabled).Error; err != nil { return err @@ -213,7 +214,7 @@ func UpdatePushEventEnabledRecord(ctx context.Context, event *model.PushEvent, e } // DeletePushEventRecord removes a push event and invalidates cache. -func DeletePushEventRecord(ctx context.Context, event *model.PushEvent) error { +func DeletePushEventRecord(ctx context.Context, event *entity.PushEvent) error { if err := GetDB(ctx).Delete(event).Error; err != nil { return err } @@ -222,8 +223,8 @@ func DeletePushEventRecord(ctx context.Context, event *model.PushEvent) error { } // ListActivePushEventsByTaskTypeRecord returns enabled events bound to a task type. -func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string) ([]model.PushEvent, error) { - var events []model.PushEvent +func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string) ([]entity.PushEvent, error) { + var events []entity.PushEvent if err := GetDB(ctx).Where("task_type = ? AND enabled = ?", taskType, true).Find(&events).Error; err != nil { return nil, err } @@ -231,8 +232,8 @@ func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string) } // GetActivePushEventByKey loads an enabled push event, preferring the cache layer. -func GetActivePushEventByKey(ctx context.Context, key string) (*model.PushEvent, error) { - event, err := getCachedOrQuery(ctx, "push:event:active:"+key, activePushEventCacheTTL, func(db *gorm.DB, dest *model.PushEvent) error { +func GetActivePushEventByKey(ctx context.Context, key string) (*entity.PushEvent, error) { + event, err := getCachedOrQuery(ctx, "push:event:active:"+key, activePushEventCacheTTL, func(db *gorm.DB, dest *entity.PushEvent) error { return db.Where("event_key = ? AND enabled = ?", key, true).First(dest).Error }) if err != nil { @@ -249,8 +250,8 @@ func DeleteActivePushEventCache(ctx context.Context, key string) { } // ListPushHistoriesRecord returns paginated push history records. -func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFilter) (int64, []model.PushHistory, error) { - query := GetDB(ctx).Model(&model.PushHistory{}).Order("created_at DESC") +func ListPushHistoriesRecord(ctx context.Context, filter do.PushHistoryListFilter) (int64, []entity.PushHistory, error) { + query := GetDB(ctx).Model(&entity.PushHistory{}).Order("created_at DESC") if filter.EventKey != "" { query = query.Where("event_key = ?", filter.EventKey) } @@ -263,7 +264,7 @@ func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFi return 0, nil, err } - var results []model.PushHistory + var results []entity.PushHistory offset := (filter.Page - 1) * filter.PageSize if err := query.Offset(offset).Limit(filter.PageSize).Find(&results).Error; err != nil { return 0, nil, err @@ -273,28 +274,23 @@ func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFi } // CreatePushHistoryRecord persists a push history audit record. -func CreatePushHistoryRecord(ctx context.Context, history *model.PushHistory) error { +func CreatePushHistoryRecord(ctx context.Context, history *entity.PushHistory) error { return GetDB(ctx).Create(history).Error } // PushHistoryQuery returns a scoped query builder for push histories. func PushHistoryQuery(ctx context.Context) *gorm.DB { - return GetDB(ctx).Model(&model.PushHistory{}) + return GetDB(ctx).Model(&entity.PushHistory{}) } // smtpConfigKeys are the system-config rows backing the built-in email channel. var smtpConfigKeys = []string{"smtp_host", "smtp_port", "smtp_username", "smtp_password"} // LoadSMTPConfigRecord reads the SMTP settings in one query. -// -// A key that is simply absent leaves its field empty, which is how an unconfigured -// mailer is represented. A read that fails is returned as an error, so callers -// cannot mistake an unhealthy database for "no SMTP configured" and silently drop -// the notification. -func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) { +func LoadSMTPConfigRecord(ctx context.Context) (do.SMTPConfig, error) { db := GetDB(ctx) if db == nil { - return model.SMTPConfig{}, errors.New("database not available") + return do.SMTPConfig{}, errors.New("database not available") } var rows []struct { @@ -305,10 +301,10 @@ func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) { Select("key", "value"). Where("key IN ?", smtpConfigKeys). Find(&rows).Error; err != nil { - return model.SMTPConfig{}, fmt.Errorf("read smtp system configs: %w", err) + return do.SMTPConfig{}, fmt.Errorf("read smtp system configs: %w", err) } - var cfg model.SMTPConfig + var cfg do.SMTPConfig for _, row := range rows { switch row.Key { case "smtp_host": @@ -325,8 +321,6 @@ func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) { } // userLookupColumns allow-lists the columns FindUserByFieldRecord may filter on. -// The column name is concatenated into the WHERE clause, so anything not listed -// here must never reach the database. var userLookupColumns = map[string]struct{}{ "id": {}, "username": {}, @@ -336,11 +330,11 @@ var userLookupColumns = map[string]struct{}{ // contract is not wired yet. field must be one of userLookupColumns. func FindUserByFieldRecord(ctx context.Context, field string, value any) (*contracts.UserDTO, error) { if _, ok := userLookupColumns[field]; !ok { - return nil, errs.ErrUnsupportedUserLookupField + return nil, consts.ErrUnsupportedUserLookupField } db := GetDB(ctx) if db == nil { - return nil, errs.ErrRecordNotFound + return nil, consts.ErrRecordNotFound } var user contracts.UserDTO if err := db.Table("w_users").Where(field+" = ?", value).First(&user).Error; err != nil { @@ -354,7 +348,7 @@ func FindUserByFieldRecord(ctx context.Context, field string, value any) (*contr func FindFirstAdminUserRecord(ctx context.Context) (*contracts.UserDTO, error) { db := GetDB(ctx) if db == nil { - return nil, errs.ErrRecordNotFound + return nil, consts.ErrRecordNotFound } var adminUser contracts.UserDTO if err := db.Table("w_users").Where("is_admin = ?", true).Order("id ASC").First(&adminUser).Error; err != nil { diff --git a/backend/plugins/domain/message_gateway/repository/push_test.go b/backend/plugins/domain/msg_gateway/dao/push_test.go similarity index 80% rename from backend/plugins/domain/message_gateway/repository/push_test.go rename to backend/plugins/domain/msg_gateway/dao/push_test.go index 18376ccd..6c8d3061 100644 --- a/backend/plugins/domain/message_gateway/repository/push_test.go +++ b/backend/plugins/domain/msg_gateway/dao/push_test.go @@ -1,12 +1,12 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package repository_test +package dao_test import ( "Wavelet/pkg/testhelper" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/repository" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/dao" "context" "errors" "testing" @@ -35,19 +35,19 @@ func TestFindUserByFieldRecordRejectsUnlistedColumns(t *testing.T) { t.Fatalf("seed user failed: %v", err) } - repository.SetDBServiceForTest(stubDBService{db: db}) - t.Cleanup(func() { repository.SetDBServiceForTest(nil) }) + dao.SetDBServiceForTest(stubDBService{db: db}) + t.Cleanup(func() { dao.SetDBServiceForTest(nil) }) ctx := context.Background() - user, err := repository.FindUserByFieldRecord(ctx, "username", "seeded") + user, err := dao.FindUserByFieldRecord(ctx, "username", "seeded") if err != nil { t.Fatalf("allowlisted lookup by username failed: %v", err) } if user.ID != 77 { t.Errorf("allowlisted lookup returned ID %d, want 77", user.ID) } - if _, err := repository.FindUserByFieldRecord(ctx, "id", uint64(77)); err != nil { + if _, err := dao.FindUserByFieldRecord(ctx, "id", uint64(77)); err != nil { t.Errorf("allowlisted lookup by id failed: %v", err) } @@ -61,7 +61,7 @@ func TestFindUserByFieldRecordRejectsUnlistedColumns(t *testing.T) { {"empty field", ""}, } for _, tc := range cases { - if _, err := repository.FindUserByFieldRecord(ctx, tc.field, "seeded"); !errors.Is(err, errs.ErrUnsupportedUserLookupField) { + if _, err := dao.FindUserByFieldRecord(ctx, tc.field, "seeded"); !errors.Is(err, consts.ErrUnsupportedUserLookupField) { t.Errorf("%s: got err %v, want ErrUnsupportedUserLookupField", tc.name, err) } } @@ -100,10 +100,10 @@ func TestLoadSMTPConfigRecordMapsEveryKey(t *testing.T) { } } - repository.SetDBServiceForTest(stubDBService{db: db}) - t.Cleanup(func() { repository.SetDBServiceForTest(nil) }) + dao.SetDBServiceForTest(stubDBService{db: db}) + t.Cleanup(func() { dao.SetDBServiceForTest(nil) }) - cfg, err := repository.LoadSMTPConfigRecord(context.Background()) + cfg, err := dao.LoadSMTPConfigRecord(context.Background()) if err != nil { t.Fatalf("LoadSMTPConfigRecord: %v", err) } @@ -122,10 +122,10 @@ func TestLoadSMTPConfigRecordSurfacesReadFailure(t *testing.T) { t.Fatalf("open bare sqlite: %v", err) } - repository.SetDBServiceForTest(stubDBService{db: bare}) - t.Cleanup(func() { repository.SetDBServiceForTest(nil) }) + dao.SetDBServiceForTest(stubDBService{db: bare}) + t.Cleanup(func() { dao.SetDBServiceForTest(nil) }) - if _, err := repository.LoadSMTPConfigRecord(context.Background()); err == nil { + if _, err := dao.LoadSMTPConfigRecord(context.Background()); err == nil { t.Fatal("LoadSMTPConfigRecord returned nil error although the config table cannot be read") } } diff --git a/backend/plugins/domain/message_gateway/migrations/postgres/00001_initial.sql b/backend/plugins/domain/msg_gateway/migrations/postgres/00001_initial.sql similarity index 100% rename from backend/plugins/domain/message_gateway/migrations/postgres/00001_initial.sql rename to backend/plugins/domain/msg_gateway/migrations/postgres/00001_initial.sql diff --git a/backend/plugins/domain/message_gateway/migrations/sqlite/00001_initial.sql b/backend/plugins/domain/msg_gateway/migrations/sqlite/00001_initial.sql similarity index 100% rename from backend/plugins/domain/message_gateway/migrations/sqlite/00001_initial.sql rename to backend/plugins/domain/msg_gateway/migrations/sqlite/00001_initial.sql diff --git a/backend/plugins/domain/msg_gateway/model/do/message.go b/backend/plugins/domain/msg_gateway/model/do/message.go new file mode 100644 index 00000000..3315cb46 --- /dev/null +++ b/backend/plugins/domain/msg_gateway/model/do/message.go @@ -0,0 +1,124 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +// Package do defines domain objects, DTOs, and request/response payloads for msg_gateway. +package do + +import "time" + +// Capability describes what an adapter can send and receive. +type Capability struct { + Text bool + Image bool + File bool + Reply bool + Group bool +} + +// ChannelConfig is the decrypted runtime config passed to a factory. +type ChannelConfig struct { + ID uint64 + Type string + Name string + Credentials map[string]string + Extra map[string]string +} + +// Recipient is the outbound destination on a platform. +type Recipient struct { + ChatID string + PlatformUserID string +} + +// Attachment is a downloaded inbound file sitting on local disk. +type Attachment struct { + Path string + FileName string + MIME string + Error string +} + +// InboundMessage is a normalized private-chat message. +type InboundMessage struct { + ChannelID uint64 + ChannelType string + PlatformUserID string + ChatID string + MessageID string + Text string + Attachments []Attachment + BindingUserID *uint64 +} + +// OutboundMessage is a reply or probe send. +type OutboundMessage struct { + Text string + ReplyToID string + Attachments []Attachment +} + +// BindRequest is the user bind body. +type BindRequest struct { + ChannelID string `json:"channel_id"` + Code string `json:"code"` +} + +// BindingDTO is a user-facing binding row. +type BindingDTO struct { + ID uint64 `json:"id,string"` + UserID uint64 `json:"user_id,string"` + ChannelID uint64 `json:"channel_id,string"` + ChannelName string `json:"channel_name"` + ChannelType string `json:"channel_type"` + PlatformUserID string `json:"platform_user_id"` + CreatedAt time.Time `json:"created_at"` +} + +// PublicChannelDTO is an enabled channel a user can bind to. +type PublicChannelDTO struct { + ID uint64 `json:"id,string"` + Name string `json:"name"` + Type string `json:"type"` +} + +// Field is one admin form field. +type Field struct { + Key string `json:"key"` + Type string `json:"type"` + Required bool `json:"required"` +} + +// Definition describes a channel type form. +type Definition struct { + Type string `json:"type"` + Fields []Field `json:"fields"` +} + +// ChannelDTO represents a channel for admin consumption. +type ChannelDTO struct { + ID uint64 `json:"id,string"` + Name string `json:"name"` + Type string `json:"type"` + OwnerScope string `json:"owner_scope"` + OwnerID *uint64 `json:"owner_id,string,omitempty"` + Enabled bool `json:"enabled"` + Credentials map[string]string `json:"credentials"` + Extra map[string]string `json:"extra"` +} + +// CreateChannelRequest is admin create payload. +type CreateChannelRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Enabled *bool `json:"enabled"` + Credentials map[string]string `json:"credentials"` + Extra map[string]string `json:"extra"` +} + +// UpdateChannelRequest is admin update payload. +type UpdateChannelRequest struct { + Name string `json:"name"` + Enabled *bool `json:"enabled"` + Credentials map[string]string `json:"credentials"` + Extra map[string]string `json:"extra"` +} diff --git a/backend/plugins/domain/message_gateway/model/push.go b/backend/plugins/domain/msg_gateway/model/do/push.go similarity index 81% rename from backend/plugins/domain/message_gateway/model/push.go rename to backend/plugins/domain/msg_gateway/model/do/push.go index 1b01deb7..1a35f4a4 100644 --- a/backend/plugins/domain/message_gateway/model/push.go +++ b/backend/plugins/domain/msg_gateway/model/do/push.go @@ -1,44 +1,13 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package model +package do import ( + "Wavelet/plugins/domain/msg_gateway/consts" + pkgpush "Wavelet/plugins/domain/msg_gateway/push" "sync" "time" - - pkgpush "Wavelet/plugins/domain/message_gateway/push" -) - -// Push channel and payload constants. -const ( - ChannelCustom = "custom" - ChannelEmail = "email" - ChannelLark = "lark" - ChannelDingTalk = "dingtalk" - ChannelTelegram = "telegram" - ChannelBark = "bark" - ChannelDiscord = "discord" - ChannelSlack = "slack" - ChannelPushover = "pushover" - DefaultLevelInfo = "INFO" - KeyTitle = "title" - KeyContent = "content" - KeyLevel = "level" - - // KeyURL represents the URL field key. - KeyURL = "url" - // KeyToken represents the Token field key. - KeyToken = "token" - // KeyOther represents the Other field key. - KeyOther = "other" - - // TypeText represents standard text input type. - TypeText = "text" - // TypePassword represents password input type. - TypePassword = "password" - // TypeTextarea represents textarea input type. - TypeTextarea = "textarea" ) // SMTPConfig mirrors the system SMTP settings consumed by the push service. @@ -140,12 +109,12 @@ type NotificationMessage struct { Ext map[string]any `json:"ext,omitempty"` } -// Flatten converts the structured NotificationMessage back to a flat map (original json structure). +// Flatten converts the structured NotificationMessage back to a flat map. func (m NotificationMessage) Flatten() map[string]any { res := map[string]any{ - KeyTitle: m.Title, - KeyContent: m.Content, - KeyLevel: m.Level, + consts.KeyTitle: m.Title, + consts.KeyContent: m.Content, + consts.KeyLevel: m.Level, } for k, v := range m.Ext { res[k] = v @@ -161,7 +130,7 @@ type EventMetadata struct { Description string `json:"description"` } -// SendPayload is the async push dispatch载荷 consumed by the notification worker. +// SendPayload is the async push dispatch payload consumed by the notification worker. type SendPayload struct { EventKey string `json:"event_key"` Config pkgpush.Config `json:"config"` @@ -181,6 +150,15 @@ type PushHistoryListFilter struct { PageSize int } +// PushNotificationEvent defines the payload for eventbus notification trigger. +type PushNotificationEvent struct { + UserID uint64 `json:"user_id"` + Channel string `json:"channel"` + Title string `json:"title"` + Content string `json:"content"` + Metadata map[string]any `json:"metadata,omitempty"` +} + var ( pushDefMu sync.RWMutex pushDefinitions = make(map[string]PushDefinition) @@ -199,15 +177,15 @@ func ListPushDefinitions() []PushDefinition { defer pushDefMu.RUnlock() order := []string{ - ChannelCustom, - ChannelLark, - ChannelDingTalk, - ChannelTelegram, - ChannelBark, - ChannelDiscord, - ChannelSlack, - ChannelPushover, - ChannelEmail, + consts.ChannelCustom, + consts.ChannelLark, + consts.ChannelDingTalk, + consts.ChannelTelegram, + consts.ChannelBark, + consts.ChannelDiscord, + consts.ChannelSlack, + consts.ChannelPushover, + consts.ChannelEmail, } res := make([]PushDefinition, 0, len(pushDefinitions)) for _, t := range order { @@ -230,25 +208,25 @@ func ListPushDefinitions() []PushDefinition { return res } -//nolint:funlen,goconst // Channel definitions registration table +//nolint:funlen,goconst,dupl // Channel definitions registration table func init() { RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelCustom, + Type: consts.ChannelCustom, Name: "自定义消息通道", Description: "使用自定义 HTTP POST 请求向外部 Webhook 发送数据。", Fields: []PushField{ { - Key: KeyURL, + Key: consts.KeyURL, Label: "请求地址", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "在此填写完整的请求地址,必须使用 HTTPS 协议", Description: "接口请求的完整 HTTPS URL,例如 https://api.example.com/webhook", }, { - Key: KeyOther, + Key: consts.KeyOther, Label: "请求体 (JSON)", - Type: TypeTextarea, + Type: consts.TypeTextarea, Required: true, Placeholder: "在此输入请求体,支持模板变量,必须为合法的 JSON 格式", Description: "可使用的变量:$title, $description, $content, $url, $to。例如 {\"text\": \"$content\"}", @@ -257,30 +235,30 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelLark, + Type: consts.ChannelLark, Name: "飞书群机器人", Description: "配置飞书群自定义机器人的 Webhook 接口投递。", Fields: []PushField{ { - Key: KeyURL, + Key: consts.KeyURL, Label: "Webhook 地址", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN", Description: "从飞书群机器人设置中复制的 Webhook URL", }, { - Key: KeyToken, + Key: consts.KeyToken, Label: "签名校验密钥 (Secret) (可选)", - Type: TypeText, + Type: consts.TypeText, Required: false, Placeholder: "可选,若机器人启用了安全设置中的签名校验,请在此输入", Description: "飞书群机器人安全设置中的签名校验 Key", }, { - Key: KeyOther, + Key: consts.KeyOther, Label: "自定义卡片 JSON 模版 (可选)", - Type: TypeTextarea, + Type: consts.TypeTextarea, Required: false, Placeholder: "可选,留空则默认使用系统内置的精美互动卡片", Description: "若填写,必须是合法的飞书卡片 JSON 格式", @@ -289,22 +267,22 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelDingTalk, + Type: consts.ChannelDingTalk, Name: "钉钉群机器人", Description: "配置钉钉群自定义机器人的 Webhook 接口投递。", Fields: []PushField{ { - Key: KeyURL, + Key: consts.KeyURL, Label: "Webhook 地址", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN", Description: "从钉钉群机器人设置中获取的完整 Webhook URL", }, { - Key: KeyToken, + Key: consts.KeyToken, Label: "加签密钥 (Secret) (可选)", - Type: TypeText, + Type: consts.TypeText, Required: false, Placeholder: "可选,若机器人启用了安全设置中的加签校验,请在此输入 SEC 开头的密钥", Description: "钉钉群机器人安全设置中的加签 Secret", @@ -313,30 +291,30 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelTelegram, + Type: consts.ChannelTelegram, Name: "Telegram 机器人", Description: "配置 Telegram 机器人推送消息。", Fields: []PushField{ { - Key: KeyURL, + Key: consts.KeyURL, Label: "API 基础地址 (可选)", - Type: TypeText, + Type: consts.TypeText, Required: false, Placeholder: "https://api.telegram.org", Description: "接口请求的 HTTPS 基础地址,留空默认为 https://api.telegram.org", }, { - Key: KeyToken, + Key: consts.KeyToken, Label: "机器人 Token (Bot Token)", - Type: TypePassword, + Type: consts.TypePassword, Required: true, Placeholder: "在此输入 Telegram 机器人的 Bot Token", Description: "通过 BotFather 申请到的机器人 Access Token", }, { - Key: KeyOther, + Key: consts.KeyOther, Label: "默认会话 ID (Chat ID) (可选)", - Type: TypeText, + Type: consts.TypeText, Required: false, Placeholder: "例如 -100123456789 或 @channel_name", Description: "默认的消息接收 Chat ID。如果通知事件中未配置 targets,将推送到此 ID", @@ -345,30 +323,30 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelBark, + Type: consts.ChannelBark, Name: "Bark (iOS 推送)", Description: "配置 Bark 推送通知至 iPhone / iPad 客户端。", Fields: []PushField{ { - Key: KeyToken, + Key: consts.KeyToken, Label: "设备 Key (Device Key)", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "Bark App 首页显示的 Device Key", Description: "从 Bark App 复制的设备专属 Key", }, { - Key: KeyURL, + Key: consts.KeyURL, Label: "Bark 服务器地址 (可选)", - Type: TypeText, + Type: consts.TypeText, Required: false, Placeholder: "https://api.day.app", Description: "Bark 服务器地址,留空默认使用官方公共服务器 https://api.day.app", }, { - Key: KeyOther, + Key: consts.KeyOther, Label: "额外配置 JSON (可选)", - Type: TypeTextarea, + Type: consts.TypeTextarea, Required: false, Placeholder: "{\"group\": \"Wavelet\", \"sound\": \"minuet\", \"icon\": \"https://...\"}", Description: "可选的 JSON 配置,支持 group (分组)、sound (铃声)、icon (自定义图标)", @@ -377,14 +355,14 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelDiscord, + Type: consts.ChannelDiscord, Name: "Discord 频道", Description: "配置 Discord 频道的 Incoming Webhook 消息推送。", Fields: []PushField{ { - Key: KeyURL, + Key: consts.KeyURL, Label: "Webhook 地址", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "https://discord.com/api/webhooks/...", Description: "从 Discord 频道集成设置中复制的 Webhook URL", @@ -393,14 +371,14 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelSlack, + Type: consts.ChannelSlack, Name: "Slack 频道", Description: "配置 Slack 频道的 Incoming Webhook 消息推送。", Fields: []PushField{ { - Key: KeyURL, + Key: consts.KeyURL, Label: "Webhook 地址", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "https://hooks.slack.com/services/...", Description: "从 Slack 应用配置中复制的 Incoming Webhook URL", @@ -409,22 +387,22 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelPushover, + Type: consts.ChannelPushover, Name: "Pushover 推送", Description: "配置 Pushover 即时推送到手机/桌面客户端。", Fields: []PushField{ { - Key: KeyToken, + Key: consts.KeyToken, Label: "应用 Token (App Token)", - Type: TypePassword, + Type: consts.TypePassword, Required: true, Placeholder: "Pushover 创建应用生成的 API Token / Key", Description: "从 Pushover 控制台创建的 Application API Token", }, { - Key: KeyURL, + Key: consts.KeyURL, Label: "用户 Key (User Key)", - Type: TypeText, + Type: consts.TypeText, Required: true, Placeholder: "Pushover 账号主页的 User Key", Description: "Pushover 个人账号的 User Key", @@ -433,7 +411,7 @@ func init() { }) RegisterPushChannelDefinition(PushDefinition{ - Type: ChannelEmail, + Type: consts.ChannelEmail, Name: "邮件推送通道", Description: "邮件推送通道直接使用系统全局 SMTP 设置进行发送,无需在此填写服务器配置。", Fields: []PushField{}, diff --git a/backend/plugins/domain/msg_gateway/model/entity/channel.go b/backend/plugins/domain/msg_gateway/model/entity/channel.go new file mode 100644 index 00000000..0ab41d1f --- /dev/null +++ b/backend/plugins/domain/msg_gateway/model/entity/channel.go @@ -0,0 +1,56 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +// Package entity defines GORM table mapping entities for msg_gateway. +package entity + +import "time" + +// MessageChannel is an admin-configured messaging adapter entity. +type MessageChannel struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + Type string `json:"type" gorm:"size:32;not null"` + Name string `json:"name" gorm:"size:64;not null"` + OwnerScope string `json:"owner_scope" gorm:"size:32;not null;default:'system'"` + OwnerID *uint64 `json:"owner_id,omitempty"` + Credentials string `json:"credentials" gorm:"type:text;not null"` + Extra string `json:"extra" gorm:"type:text"` + Enabled bool `json:"enabled" gorm:"default:false;not null"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +// TableName 表名 +func (MessageChannel) TableName() string { + return "w_message_channels" +} + +// MessageBinding maps a platform user to a Wavelet user on one channel. +type MessageBinding struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + ChannelID uint64 `json:"channel_id" gorm:"not null;index"` + PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"` + UserID uint64 `json:"user_id" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` +} + +// TableName 表名 +func (MessageBinding) TableName() string { + return "w_message_bindings" +} + +// MessagePairingCode is a one-time bind code. +type MessagePairingCode struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + Code string `json:"code" gorm:"size:32;uniqueIndex;not null"` + ChannelID uint64 `json:"channel_id" gorm:"not null;index"` + PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"` + UserID uint64 `json:"user_id" gorm:"not null;index"` + ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` +} + +// TableName 表名 +func (MessagePairingCode) TableName() string { + return "w_message_pairing_codes" +} diff --git a/backend/plugins/domain/msg_gateway/model/entity/push.go b/backend/plugins/domain/msg_gateway/model/entity/push.go new file mode 100644 index 00000000..8fb478dd --- /dev/null +++ b/backend/plugins/domain/msg_gateway/model/entity/push.go @@ -0,0 +1,95 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package entity + +import ( + "Wavelet/plugins/domain/msg_gateway/consts" + "errors" + "strings" + "time" +) + +// PushChannel 消息通道实体 +type PushChannel struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"size:100;not null"` + Description string `json:"description" gorm:"size:255"` + Type string `json:"type" gorm:"size:50;not null;index"` + URL string `json:"url" gorm:"type:text"` + Token string `json:"token" gorm:"type:text"` + Other string `json:"other" gorm:"type:text"` + Enabled bool `json:"enabled" gorm:"index;not null;default:true"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` +} + +// TableName 指定 GORM 表名 +func (PushChannel) TableName() string { + return "w_push_channels" +} + +// Validate 验证与标准化字段 +func (c *PushChannel) Validate() error { + c.Name = strings.TrimSpace(c.Name) + if c.Name == "" { + return errors.New(consts.ErrChannelNameRequired) + } + c.Type = strings.TrimSpace(c.Type) + if c.Type == "" { + return errors.New(consts.ErrChannelTypeRequired) + } + return nil +} + +// PushEvent 系统通知事件实体 +type PushEvent struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + EventKey string `json:"event_key" gorm:"uniqueIndex;size:80;not null"` + Name string `json:"name" gorm:"size:100;not null"` + TaskType string `json:"task_type" gorm:"size:100;index;not null;default:''"` + Channels []string `json:"channels" gorm:"type:text;serializer:json;not null"` + Targets []string `json:"targets" gorm:"type:text;serializer:json;not null"` + Template string `json:"template" gorm:"type:text;not null"` + Enabled bool `json:"enabled" gorm:"index;not null;default:false"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` +} + +// TableName 指定 GORM 表名 +func (PushEvent) TableName() string { + return "w_push_events" +} + +// Validate 验证 PushEvent 实体字段 +func (e *PushEvent) Validate() error { + e.EventKey = strings.TrimSpace(e.EventKey) + if e.EventKey == "" { + return errors.New(consts.ErrEventKeyRequired) + } + e.Name = strings.TrimSpace(e.Name) + if e.Name == "" { + return errors.New(consts.ErrNameRequired) + } + return nil +} + +// PushHistory 推送日志/历史实体 +type PushHistory struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + EventKey string `json:"event_key" gorm:"size:80;not null;index"` + Channel string `json:"channel" gorm:"size:50;not null;index"` + Target string `json:"target" gorm:"size:255;not null"` + Title string `json:"title" gorm:"size:255;not null"` + Content string `json:"content" gorm:"type:text;not null"` + Level string `json:"level" gorm:"size:20;not null;default:'INFO'"` + Status string `json:"status" gorm:"size:20;not null;index"` + ErrorMsg string `json:"error_msg" gorm:"type:text"` + Payload string `json:"payload" gorm:"type:text"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` +} + +// TableName 指定 GORM 表名 +func (PushHistory) TableName() string { + return "w_push_histories" +} diff --git a/backend/plugins/domain/message_gateway/message_gateway_test.go b/backend/plugins/domain/msg_gateway/msg_gateway_test.go similarity index 66% rename from backend/plugins/domain/message_gateway/message_gateway_test.go rename to backend/plugins/domain/msg_gateway/msg_gateway_test.go index cec83794..f3f72508 100644 --- a/backend/plugins/domain/message_gateway/message_gateway_test.go +++ b/backend/plugins/domain/msg_gateway/msg_gateway_test.go @@ -1,11 +1,11 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package message_gateway_test +package msg_gateway_test import ( "Wavelet/pkg/testhelper" - "Wavelet/plugins/domain/message_gateway" + "Wavelet/plugins/domain/msg_gateway" "context" "testing" "time" @@ -31,17 +31,17 @@ func (m *mockDBService) Named(_ string) *gorm.DB { func TestUpsertPairingCode_ReusesUnexpired(t *testing.T) { testDB, _, cleanup := testhelper.SetupTestEnvironment(t) - message_gateway.SetDBServiceForTest(&mockDBService{db: testDB}) + msg_gateway.SetDBServiceForTest(&mockDBService{db: testDB}) defer func() { - message_gateway.SetDBServiceForTest(nil) + msg_gateway.SetDBServiceForTest(nil) cleanup() }() ctx := context.Background() - first, err := message_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ABCD1234", time.Now().Add(15*time.Minute)) + first, err := msg_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ABCD1234", time.Now().Add(15*time.Minute)) if err != nil { t.Fatal(err) } - second, err := message_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ZZZZ9999", time.Now().Add(15*time.Minute)) + second, err := msg_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ZZZZ9999", time.Now().Add(15*time.Minute)) if err != nil { t.Fatal(err) } diff --git a/backend/plugins/domain/message_gateway/pairing_test.go b/backend/plugins/domain/msg_gateway/pairing_test.go similarity index 96% rename from backend/plugins/domain/message_gateway/pairing_test.go rename to backend/plugins/domain/msg_gateway/pairing_test.go index c442c30d..e047db88 100644 --- a/backend/plugins/domain/message_gateway/pairing_test.go +++ b/backend/plugins/domain/msg_gateway/pairing_test.go @@ -1,7 +1,7 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package message_gateway +package msg_gateway import ( "strings" diff --git a/backend/plugins/domain/message_gateway/plugin.go b/backend/plugins/domain/msg_gateway/plugin.go similarity index 60% rename from backend/plugins/domain/message_gateway/plugin.go rename to backend/plugins/domain/msg_gateway/plugin.go index 038e5aa3..1188ae9a 100644 --- a/backend/plugins/domain/message_gateway/plugin.go +++ b/backend/plugins/domain/msg_gateway/plugin.go @@ -1,8 +1,8 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -// Package message_gateway provides the Bot gateway, multi-channel notification dispatching, and asynchronous push worker domain plugin for Cordis. -package message_gateway +// Package msg_gateway provides the Bot gateway, multi-channel notification dispatching, and asynchronous push worker domain plugin for Cordis. +package msg_gateway import ( "Wavelet/core" @@ -10,12 +10,14 @@ import ( "Wavelet/core/extpoints" "Wavelet/pkg/ginutil" "Wavelet/pkg/util" - "Wavelet/plugins/domain/message_gateway/channels/qq" - "Wavelet/plugins/domain/message_gateway/channels/telegram" - "Wavelet/plugins/domain/message_gateway/handler" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/repository" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/channels/qq" + "Wavelet/plugins/domain/msg_gateway/channels/telegram" + "Wavelet/plugins/domain/msg_gateway/consts" + "Wavelet/plugins/domain/msg_gateway/controller" + "Wavelet/plugins/domain/msg_gateway/dao" + "Wavelet/plugins/domain/msg_gateway/model/do" + "Wavelet/plugins/domain/msg_gateway/model/entity" + "Wavelet/plugins/domain/msg_gateway/service" "context" "embed" "reflect" @@ -26,7 +28,7 @@ import ( //go:embed migrations/*/*.sql var mgMigrations embed.FS -// Option configures the message_gateway plugin. +// Option configures the msg_gateway plugin. type Option func(*Plugin) // WithAutoStartRunner enables automatic bot runner startup in the background. @@ -42,7 +44,7 @@ type Plugin struct { cancelRunner context.CancelFunc } -// New creates a new message_gateway domain plugin. +// New creates a new msg_gateway domain plugin. func New(opts ...Option) *Plugin { p := &Plugin{} for _, opt := range opts { @@ -53,12 +55,12 @@ func New(opts ...Option) *Plugin { return p } -// Name returns the unique identifier for the message_gateway domain plugin. +// Name returns the unique identifier for the msg_gateway domain plugin. func (p *Plugin) Name() string { - return "message_gateway" + return "msg_gateway" } -// Inject declares required dependencies for the message_gateway domain plugin. +// Inject declares required dependencies for the msg_gateway domain plugin. func (p *Plugin) Inject() []reflect.Type { return []reflect.Type{ reflect.TypeFor[contracts.DBService](), @@ -72,7 +74,7 @@ func (p *Plugin) Inject() []reflect.Type { // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ - Name: "message_gateway", + Name: "msg_gateway", Version: "1.0.0", Description: "Bot gateway, multi-channel notification push, and async worker dispatch plugin", Author: "Wavelet Team", @@ -83,29 +85,29 @@ type mgAppConfig struct { SessionSecret string `config:"session_secret" env:"APP_SESSION_SECRET" secret:"true"` } -// DeclareConfig declares configuration bindings for the message_gateway plugin. +// DeclareConfig declares configuration bindings for the msg_gateway plugin. func (p *Plugin) DeclareConfig() []core.ConfigBinding { return []core.ConfigBinding{ {Prefix: "app", Target: &mgAppConfig{}}, } } -// Apply registers message_gateway migrations, routes, tasks, schedules, events, and settings into the Context. +// Apply registers msg_gateway migrations, routes, tasks, schedules, events, and settings into the Context. func (p *Plugin) Apply(ctx *core.Context) error { var cfg mgAppConfig if err := ctx.Config().Bind("app", &cfg); err == nil && cfg.SessionSecret != "" { service.SetCredentialSecret(cfg.SessionSecret) } - core.Bind[contracts.DBService](ctx, repository.SetDBService) + core.Bind[contracts.DBService](ctx, dao.SetDBService) core.Bind[contracts.CacheService](ctx, func(cache contracts.CacheService) { - repository.SetCacheService(cache) + dao.SetCacheService(cache) service.SetCacheService(cache) }) core.Bind[contracts.TaskService](ctx, service.SetTaskService) core.Bind[contracts.UserService](ctx, service.SetUserService) ctx.OnDispose(func() error { - repository.SetDBService(nil) - repository.SetCacheService(nil) + dao.SetDBService(nil) + dao.SetCacheService(nil) service.SetCacheService(nil) service.SetTaskService(nil) service.SetUserService(nil) @@ -126,25 +128,25 @@ func (p *Plugin) Apply(ctx *core.Context) error { } // 1. Register migrations - ctx.Migrations().Register("message_gateway", mgMigrations) + ctx.Migrations().Register("msg_gateway", mgMigrations) // 2. Register User HTTP Routes - handler.RegisterUserRoutes(ctx.Router().Group("/api/v1"), loginMW) + controller.RegisterUserRoutes(ctx.Router().Group("/api/v1"), loginMW) // 3. Register Admin Message Gateway HTTP Routes - handler.RegisterAdminRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW) + controller.RegisterAdminRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW) // 4. Register Admin Push HTTP Routes - handler.RegisterAdminPushRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW) + controller.RegisterAdminPushRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW) - service.Register(model.MessageChannelTypeTelegram, telegram.New) - service.Register(model.MessageChannelTypeQQ, qq.New) + service.Register(consts.MessageChannelTypeTelegram, telegram.New) + service.Register(consts.MessageChannelTypeQQ, qq.New) const defaultTaskRetry = 3 pushHandler := &service.PushHandler{} // 5. Register background tasks - ctx.Task().Register("message_gateway:push_notification", func(c context.Context, payload []byte) error { + ctx.Task().Register(consts.TaskPushNotification, func(c context.Context, payload []byte) error { return pushHandler.Execute(c, payload) }, extpoints.WithTaskType("push_notification"), @@ -163,8 +165,8 @@ func (p *Plugin) Apply(ctx *core.Context) error { ctx.Task().Register(service.TaskDispatchBotMsg, &service.BotDispatchHandler{}, extpoints.WithTaskMeta(service.BotDispatchMeta)) - ctx.Task().Register("message_gateway:cleanup_pairing_codes", func(c context.Context, _ []byte) error { - return repository.DeleteExpiredPairingCodes(c) + ctx.Task().Register(consts.TaskCleanupPairingCodes, func(c context.Context, _ []byte) error { + return dao.DeleteExpiredPairingCodes(c) }, extpoints.WithTaskType("cleanup_pairing_codes"), extpoints.WithTaskName("清理过期配对码"), @@ -176,17 +178,17 @@ func (p *Plugin) Apply(ctx *core.Context) error { ) // 6. Register Cron Schedules - ctx.Schedule().RegisterCron("*/10 * * * *", "message_gateway:cleanup_pairing_codes", map[string]any{"action": "cleanup"}) + ctx.Schedule().RegisterCron("*/10 * * * *", consts.TaskCleanupPairingCodes, map[string]any{"action": "cleanup"}) // 7. Register EventBus listeners for decoupled push triggers - ctx.Events().On("notification:push", func(c context.Context, e model.PushNotificationEvent) error { - meta := model.EventMetadata{ + ctx.Events().On("notification:push", func(c context.Context, e do.PushNotificationEvent) error { + meta := do.EventMetadata{ Key: "eventbus:" + e.Channel, Name: e.Title, - DefaultTemplate: model.NotificationMessage{ + DefaultTemplate: do.NotificationMessage{ Title: e.Title, Content: e.Content, - Level: model.DefaultLevelInfo, + Level: consts.DefaultLevelInfo, Ext: e.Metadata, }, Description: "EventBus triggered notification", @@ -211,14 +213,14 @@ func (p *Plugin) Apply(ctx *core.Context) error { // 10. Register Settings Schemas ctx.Settings().Register(extpoints.SettingSchema{ - Key: "message_gateway.pairing_code_expiry_minutes", + Key: "msg_gateway.pairing_code_expiry_minutes", Default: 15, Description: "Expiry duration for bot pairing codes in minutes", Type: "integer", Category: "messaging", }) ctx.Settings().Register(extpoints.SettingSchema{ - Key: "message_gateway.max_bindings_per_user", + Key: "msg_gateway.max_bindings_per_user", Default: 5, Description: "Maximum platform bot bindings per user", Type: "integer", @@ -250,77 +252,77 @@ const ( CodeLength = service.CodeLength ) -// MessageChannel is an alias for model.MessageChannel. -type MessageChannel = model.MessageChannel +// MessageChannel is an alias for entity.MessageChannel. +type MessageChannel = entity.MessageChannel -// MessageBinding is an alias for model.MessageBinding. -type MessageBinding = model.MessageBinding +// MessageBinding is an alias for entity.MessageBinding. +type MessageBinding = entity.MessageBinding -// MessagePairingCode is an alias for model.MessagePairingCode. -type MessagePairingCode = model.MessagePairingCode +// MessagePairingCode is an alias for entity.MessagePairingCode. +type MessagePairingCode = entity.MessagePairingCode -// PushChannel is an alias for model.PushChannel. -type PushChannel = model.PushChannel +// PushChannel is an alias for entity.PushChannel. +type PushChannel = entity.PushChannel -// PushEvent is an alias for model.PushEvent. -type PushEvent = model.PushEvent +// PushEvent is an alias for entity.PushEvent. +type PushEvent = entity.PushEvent -// PushHistory is an alias for model.PushHistory. -type PushHistory = model.PushHistory +// PushHistory is an alias for entity.PushHistory. +type PushHistory = entity.PushHistory -// PushNotificationEvent is an alias for model.PushNotificationEvent. -type PushNotificationEvent = model.PushNotificationEvent +// PushNotificationEvent is an alias for do.PushNotificationEvent. +type PushNotificationEvent = do.PushNotificationEvent -// ChannelConfig is an alias for model.ChannelConfig. -type ChannelConfig = model.ChannelConfig +// ChannelConfig is an alias for do.ChannelConfig. +type ChannelConfig = do.ChannelConfig -// Capability is an alias for model.Capability. -type Capability = model.Capability +// Capability is an alias for do.Capability. +type Capability = do.Capability -// Recipient is an alias for model.Recipient. -type Recipient = model.Recipient +// Recipient is an alias for do.Recipient. +type Recipient = do.Recipient -// Attachment is an alias for model.Attachment. -type Attachment = model.Attachment +// Attachment is an alias for do.Attachment. +type Attachment = do.Attachment -// InboundMessage is an alias for model.InboundMessage. -type InboundMessage = model.InboundMessage +// InboundMessage is an alias for do.InboundMessage. +type InboundMessage = do.InboundMessage -// OutboundMessage is an alias for model.OutboundMessage. -type OutboundMessage = model.OutboundMessage +// OutboundMessage is an alias for do.OutboundMessage. +type OutboundMessage = do.OutboundMessage -// BindingDTO is an alias for model.BindingDTO. -type BindingDTO = model.BindingDTO +// BindingDTO is an alias for do.BindingDTO. +type BindingDTO = do.BindingDTO -// PublicChannelDTO is an alias for model.PublicChannelDTO. -type PublicChannelDTO = model.PublicChannelDTO +// PublicChannelDTO is an alias for do.PublicChannelDTO. +type PublicChannelDTO = do.PublicChannelDTO -// Definition is an alias for model.Definition. -type Definition = model.Definition +// Definition is an alias for do.Definition. +type Definition = do.Definition -// ChannelDTO is an alias for model.ChannelDTO. -type ChannelDTO = model.ChannelDTO +// ChannelDTO is an alias for do.ChannelDTO. +type ChannelDTO = do.ChannelDTO -// CreateChannelRequest is an alias for model.CreateChannelRequest. -type CreateChannelRequest = model.CreateChannelRequest +// CreateChannelRequest is an alias for do.CreateChannelRequest. +type CreateChannelRequest = do.CreateChannelRequest -// UpdateChannelRequest is an alias for model.UpdateChannelRequest. -type UpdateChannelRequest = model.UpdateChannelRequest +// UpdateChannelRequest is an alias for do.UpdateChannelRequest. +type UpdateChannelRequest = do.UpdateChannelRequest -// PushDefinition is an alias for model.PushDefinition. -type PushDefinition = model.PushDefinition +// PushDefinition is an alias for do.PushDefinition. +type PushDefinition = do.PushDefinition -// PushField is an alias for model.PushField. -type PushField = model.PushField +// PushField is an alias for do.PushField. +type PushField = do.PushField -// NotificationMessage is an alias for model.NotificationMessage. -type NotificationMessage = model.NotificationMessage +// NotificationMessage is an alias for do.NotificationMessage. +type NotificationMessage = do.NotificationMessage -// EventMetadata is an alias for model.EventMetadata. -type EventMetadata = model.EventMetadata +// EventMetadata is an alias for do.EventMetadata. +type EventMetadata = do.EventMetadata -// SendPayload is an alias for model.SendPayload. -type SendPayload = model.SendPayload +// SendPayload is an alias for do.SendPayload. +type SendPayload = do.SendPayload // Handler is an alias for service.Handler. type Handler = service.Handler @@ -342,8 +344,8 @@ type PushHandler = service.PushHandler // Re-exported variables and functions. var ( - SetDBServiceForTest = repository.SetDBServiceForTest - UpsertPairingCode = repository.UpsertPairingCode + SetDBServiceForTest = dao.SetDBServiceForTest + UpsertPairingCode = dao.UpsertPairingCode Register = service.Register Lookup = service.Lookup GenerateCode = service.GenerateCode diff --git a/backend/plugins/domain/message_gateway/plugin_registry_test.go b/backend/plugins/domain/msg_gateway/plugin_registry_test.go similarity index 89% rename from backend/plugins/domain/message_gateway/plugin_registry_test.go rename to backend/plugins/domain/msg_gateway/plugin_registry_test.go index 57cd9205..67a8cfe5 100644 --- a/backend/plugins/domain/message_gateway/plugin_registry_test.go +++ b/backend/plugins/domain/msg_gateway/plugin_registry_test.go @@ -1,13 +1,13 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package message_gateway_test +package msg_gateway_test import ( "Wavelet/core" "Wavelet/core/contracts" - "Wavelet/plugins/domain/message_gateway" - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway" + "Wavelet/plugins/domain/msg_gateway/service" "context" "testing" @@ -17,7 +17,7 @@ import ( func TestPushRegistry(t *testing.T) { ctx := core.NewContext(context.Background()) - require.NoError(t, message_gateway.New().Apply(ctx)) + require.NoError(t, msg_gateway.New().Apply(ctx)) registry, err := core.Inject[contracts.PushRegistry](ctx) require.NoError(t, err) diff --git a/backend/plugins/domain/message_gateway/plugin_test.go b/backend/plugins/domain/msg_gateway/plugin_test.go similarity index 71% rename from backend/plugins/domain/message_gateway/plugin_test.go rename to backend/plugins/domain/msg_gateway/plugin_test.go index 3a6f8ef2..14bbc5f9 100644 --- a/backend/plugins/domain/message_gateway/plugin_test.go +++ b/backend/plugins/domain/msg_gateway/plugin_test.go @@ -1,11 +1,11 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package message_gateway_test +package msg_gateway_test import ( "Wavelet/core" - "Wavelet/plugins/domain/message_gateway" + "Wavelet/plugins/domain/msg_gateway" "context" "io/fs" "testing" @@ -14,32 +14,32 @@ import ( "github.com/stretchr/testify/require" ) -func TestMessageGatewayPluginUnit(t *testing.T) { +func TestMsgGatewayPluginUnit(t *testing.T) { ctx := core.NewContext(context.Background()) - p := message_gateway.New() - assert.Equal(t, "message_gateway", p.Name()) + p := msg_gateway.New() + assert.Equal(t, "msg_gateway", p.Name()) assert.Equal(t, "1.0.0", p.Manifest().Version) require.NoError(t, p.Apply(ctx)) // Verify migrations - entry, ok := ctx.Migrations().Get("message_gateway") + entry, ok := ctx.Migrations().Get("msg_gateway") require.True(t, ok) entries, err := fs.ReadDir(entry.FS, entry.Dir) require.NoError(t, err) assert.NotEmpty(t, entries) // Verify tasks - task, ok := ctx.Tasks().Get("message_gateway:push_notification") + task, ok := ctx.Tasks().Get("msg_gateway:push_notification") require.True(t, ok) assert.Equal(t, 3, task.Retry) // Verify schedules - sched, ok := ctx.Schedules().Get("message_gateway:cleanup_pairing_codes") + sched, ok := ctx.Schedules().Get("msg_gateway:cleanup_pairing_codes") require.True(t, ok) assert.Equal(t, "*/10 * * * *", sched.Spec) // Verify settings - setting, ok := ctx.Settings().Get("message_gateway.max_bindings_per_user") + setting, ok := ctx.Settings().Get("msg_gateway.max_bindings_per_user") require.True(t, ok) assert.Equal(t, 5, setting.Default) } @@ -48,7 +48,7 @@ func TestMessageGatewayPluginUnit(t *testing.T) { // Register,则每次触发都投递到无人处理的任务类型,清理逻辑静默失效。 func TestEveryScheduleHasTaskHandler(t *testing.T) { ctx := core.NewContext(context.Background()) - require.NoError(t, message_gateway.New().Apply(ctx)) + require.NoError(t, msg_gateway.New().Apply(ctx)) schedules := ctx.Schedules().Schedules() require.NotEmpty(t, schedules) diff --git a/backend/plugins/domain/message_gateway/push/bark.go b/backend/plugins/domain/msg_gateway/push/bark.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/bark.go rename to backend/plugins/domain/msg_gateway/push/bark.go diff --git a/backend/plugins/domain/message_gateway/push/channels_test.go b/backend/plugins/domain/msg_gateway/push/channels_test.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/channels_test.go rename to backend/plugins/domain/msg_gateway/push/channels_test.go diff --git a/backend/plugins/domain/message_gateway/push/custom.go b/backend/plugins/domain/msg_gateway/push/custom.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/custom.go rename to backend/plugins/domain/msg_gateway/push/custom.go diff --git a/backend/plugins/domain/message_gateway/push/custom_test.go b/backend/plugins/domain/msg_gateway/push/custom_test.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/custom_test.go rename to backend/plugins/domain/msg_gateway/push/custom_test.go diff --git a/backend/plugins/domain/message_gateway/push/dingtalk.go b/backend/plugins/domain/msg_gateway/push/dingtalk.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/dingtalk.go rename to backend/plugins/domain/msg_gateway/push/dingtalk.go diff --git a/backend/plugins/domain/message_gateway/push/discord.go b/backend/plugins/domain/msg_gateway/push/discord.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/discord.go rename to backend/plugins/domain/msg_gateway/push/discord.go diff --git a/backend/plugins/domain/message_gateway/push/email.go b/backend/plugins/domain/msg_gateway/push/email.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/email.go rename to backend/plugins/domain/msg_gateway/push/email.go diff --git a/backend/plugins/domain/message_gateway/push/email_test.go b/backend/plugins/domain/msg_gateway/push/email_test.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/email_test.go rename to backend/plugins/domain/msg_gateway/push/email_test.go diff --git a/backend/plugins/domain/message_gateway/push/lark.go b/backend/plugins/domain/msg_gateway/push/lark.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/lark.go rename to backend/plugins/domain/msg_gateway/push/lark.go diff --git a/backend/plugins/domain/message_gateway/push/push.go b/backend/plugins/domain/msg_gateway/push/push.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/push.go rename to backend/plugins/domain/msg_gateway/push/push.go diff --git a/backend/plugins/domain/message_gateway/push/pushover.go b/backend/plugins/domain/msg_gateway/push/pushover.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/pushover.go rename to backend/plugins/domain/msg_gateway/push/pushover.go diff --git a/backend/plugins/domain/message_gateway/push/slack.go b/backend/plugins/domain/msg_gateway/push/slack.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/slack.go rename to backend/plugins/domain/msg_gateway/push/slack.go diff --git a/backend/plugins/domain/message_gateway/push/telegram.go b/backend/plugins/domain/msg_gateway/push/telegram.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/telegram.go rename to backend/plugins/domain/msg_gateway/push/telegram.go diff --git a/backend/plugins/domain/message_gateway/push/telegram_test.go b/backend/plugins/domain/msg_gateway/push/telegram_test.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/telegram_test.go rename to backend/plugins/domain/msg_gateway/push/telegram_test.go diff --git a/backend/plugins/domain/message_gateway/push/template.go b/backend/plugins/domain/msg_gateway/push/template.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/template.go rename to backend/plugins/domain/msg_gateway/push/template.go diff --git a/backend/plugins/domain/message_gateway/push/template_test.go b/backend/plugins/domain/msg_gateway/push/template_test.go similarity index 100% rename from backend/plugins/domain/message_gateway/push/template_test.go rename to backend/plugins/domain/msg_gateway/push/template_test.go diff --git a/backend/plugins/domain/message_gateway/registry_test.go b/backend/plugins/domain/msg_gateway/registry_test.go similarity index 97% rename from backend/plugins/domain/message_gateway/registry_test.go rename to backend/plugins/domain/msg_gateway/registry_test.go index ad86d322..5431f0c6 100644 --- a/backend/plugins/domain/message_gateway/registry_test.go +++ b/backend/plugins/domain/msg_gateway/registry_test.go @@ -1,7 +1,7 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package message_gateway +package msg_gateway import ( "context" diff --git a/backend/plugins/domain/message_gateway/service/admin.go b/backend/plugins/domain/msg_gateway/service/admin.go similarity index 65% rename from backend/plugins/domain/message_gateway/service/admin.go rename to backend/plugins/domain/msg_gateway/service/admin.go index d785d3a9..15150fb8 100644 --- a/backend/plugins/domain/message_gateway/service/admin.go +++ b/backend/plugins/domain/msg_gateway/service/admin.go @@ -4,9 +4,10 @@ package service import ( - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/repository" + "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" @@ -22,19 +23,19 @@ import ( const defaultTelegramAPI = "https://api.telegram.org" // ListDefinitions returns the admin form schema of every supported channel type. -func ListDefinitions() []model.Definition { - return []model.Definition{ +func ListDefinitions() []do.Definition { + return []do.Definition{ { - Type: model.MessageChannelTypeTelegram, - Fields: []model.Field{ - {Key: "token", Type: model.TypePassword, Required: true}, - {Key: "api_base", Type: model.TypeText, Required: false}, + Type: consts.MessageChannelTypeTelegram, + Fields: []do.Field{ + {Key: "token", Type: consts.TypePassword, Required: true}, + {Key: "api_base", Type: consts.TypeText, Required: false}, }, }, { - Type: model.MessageChannelTypeQQ, - Fields: []model.Field{ - {Key: "app_id", Type: model.TypeText, Required: true}, + Type: consts.MessageChannelTypeQQ, + Fields: []do.Field{ + {Key: "app_id", Type: consts.TypeText, Required: true}, {Key: "client_secret", Type: "password", Required: true}, }, }, @@ -42,25 +43,25 @@ func ListDefinitions() []model.Definition { } // CreateChannel validates the admin payload and persists an encrypted channel. -func CreateChannel(ctx context.Context, req model.CreateChannelRequest) (model.ChannelDTO, error) { +func CreateChannel(ctx context.Context, req do.CreateChannelRequest) (do.ChannelDTO, error) { name := strings.TrimSpace(req.Name) if name == "" { - return model.ChannelDTO{}, errors.New(errs.ErrNameRequired) + return do.ChannelDTO{}, errors.New(consts.ErrNameRequired) } channelType := strings.TrimSpace(req.Type) - if channelType != model.MessageChannelTypeTelegram && channelType != model.MessageChannelTypeQQ { - return model.ChannelDTO{}, errors.New(errs.ErrTypeInvalid) + if channelType != consts.MessageChannelTypeTelegram && channelType != consts.MessageChannelTypeQQ { + return do.ChannelDTO{}, errors.New(consts.ErrTypeInvalid) } creds := req.Credentials if creds == nil { creds = map[string]string{} } if err := ValidateCredentials(channelType, creds, false); err != nil { - return model.ChannelDTO{}, err + return do.ChannelDTO{}, err } cipher, err := EncryptCredentials(creds) if err != nil { - return model.ChannelDTO{}, err + return do.ChannelDTO{}, err } extra := req.Extra if extra == nil { @@ -70,32 +71,32 @@ func CreateChannel(ctx context.Context, req model.CreateChannelRequest) (model.C if req.Enabled != nil { enabled = *req.Enabled } - row := &model.MessageChannel{ + row := &entity.MessageChannel{ Name: name, Type: channelType, - OwnerScope: model.MessageOwnerScopeSystem, + OwnerScope: consts.MessageOwnerScopeSystem, Enabled: enabled, Credentials: cipher, Extra: EncodeExtra(extra), } - if err := repository.CreateMessageChannel(ctx, row); err != nil { - return model.ChannelDTO{}, err + if err := dao.CreateMessageChannel(ctx, row); err != nil { + return do.ChannelDTO{}, err } return ToDTO(row, creds, extra), nil } // UpdateChannel patches a channel; empty secrets keep the stored ciphertext. -func UpdateChannel(ctx context.Context, id uint64, req model.UpdateChannelRequest) (model.ChannelDTO, error) { - row, err := repository.GetMessageChannel(ctx, id) +func UpdateChannel(ctx context.Context, id uint64, req do.UpdateChannelRequest) (do.ChannelDTO, error) { + row, err := dao.GetMessageChannel(ctx, id) if err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { - return model.ChannelDTO{}, errors.New(errs.ErrChannelNotFound) + if errors.Is(err, consts.ErrRecordNotFound) { + return do.ChannelDTO{}, errors.New(consts.ErrChannelNotFound) } - return model.ChannelDTO{}, err + return do.ChannelDTO{}, err } creds, err := DecryptCredentials(row.Credentials) if err != nil { - return model.ChannelDTO{}, err + return do.ChannelDTO{}, err } extra := ParseExtra(row.Extra) @@ -120,30 +121,30 @@ func UpdateChannel(ctx context.Context, id uint64, req model.UpdateChannelReques merged[k] = v } if err := ValidateCredentials(row.Type, merged, true); err != nil { - return model.ChannelDTO{}, err + return do.ChannelDTO{}, err } creds = merged } cipher, err := EncryptCredentials(creds) if err != nil { - return model.ChannelDTO{}, err + return do.ChannelDTO{}, err } row.Credentials = cipher row.Extra = EncodeExtra(extra) - if err := repository.UpdateMessageChannel(ctx, row); err != nil { - return model.ChannelDTO{}, err + if err := dao.UpdateMessageChannel(ctx, row); err != nil { + return do.ChannelDTO{}, err } return ToDTO(row, creds, extra), nil } // ListChannels returns every channel with secrets masked. -func ListChannels(ctx context.Context) ([]model.ChannelDTO, error) { - rows, err := repository.ListMessageChannels(ctx) +func ListChannels(ctx context.Context) ([]do.ChannelDTO, error) { + rows, err := dao.ListMessageChannels(ctx) if err != nil { return nil, err } - out := make([]model.ChannelDTO, 0, len(rows)) + out := make([]do.ChannelDTO, 0, len(rows)) for i := range rows { creds, _ := DecryptCredentials(rows[i].Credentials) extra := ParseExtra(rows[i].Extra) @@ -154,21 +155,21 @@ func ListChannels(ctx context.Context) ([]model.ChannelDTO, error) { // DeleteChannel removes a channel together with its bindings and pairing codes. func DeleteChannel(ctx context.Context, id uint64) error { - if _, err := repository.GetMessageChannel(ctx, id); err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { - return errors.New(errs.ErrChannelNotFound) + if _, err := dao.GetMessageChannel(ctx, id); err != nil { + if errors.Is(err, consts.ErrRecordNotFound) { + return errors.New(consts.ErrChannelNotFound) } return err } - return repository.DeleteMessageChannel(ctx, id) + return dao.DeleteMessageChannel(ctx, id) } // ProbeChannel verifies the stored credentials against the upstream platform. func ProbeChannel(ctx context.Context, id uint64) error { - row, err := repository.GetMessageChannel(ctx, id) + row, err := dao.GetMessageChannel(ctx, id) if err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { - return errors.New(errs.ErrChannelNotFound) + if errors.Is(err, consts.ErrRecordNotFound) { + return errors.New(consts.ErrChannelNotFound) } return err } @@ -177,12 +178,12 @@ func ProbeChannel(ctx context.Context, id uint64) error { return err } switch row.Type { - case model.MessageChannelTypeTelegram: + case consts.MessageChannelTypeTelegram: return ProbeTelegram(ctx, creds) - case model.MessageChannelTypeQQ: + case consts.MessageChannelTypeQQ: return ProbeQQ(ctx, creds) default: - return errors.New(errs.ErrTypeInvalid) + return errors.New(consts.ErrTypeInvalid) } } @@ -190,7 +191,7 @@ func ProbeChannel(ctx context.Context, id uint64) error { func ProbeTelegram(ctx context.Context, creds map[string]string) error { tok := creds["token"] if strings.TrimSpace(tok) == "" { - return errors.New(errs.ErrMissingTelegramToken) + return errors.New(consts.ErrMissingTelegramToken) } base := creds["api_base"] base = strings.TrimRight(strings.TrimSpace(base), "/") @@ -210,7 +211,7 @@ func ProbeTelegram(ctx context.Context, creds map[string]string) error { defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { - return fmt.Errorf("%s (%d): %s", errs.ErrTelegramGetMeFailed, resp.StatusCode, string(body)) + return fmt.Errorf("%s (%d): %s", consts.ErrTelegramGetMeFailed, resp.StatusCode, string(body)) } var res struct { OK bool `json:"ok"` @@ -219,7 +220,7 @@ func ProbeTelegram(ctx context.Context, creds map[string]string) error { return err } if !res.OK { - return fmt.Errorf("%s: %s", errs.ErrTelegramNotOK, string(body)) + return fmt.Errorf("%s: %s", consts.ErrTelegramNotOK, string(body)) } return nil } @@ -229,7 +230,7 @@ func ProbeQQ(_ context.Context, creds map[string]string) error { appID := strings.TrimSpace(creds["app_id"]) secret := strings.TrimSpace(creds["client_secret"]) if appID == "" || secret == "" { - return errors.New(errs.ErrMissingQQCredentials) + return errors.New(consts.ErrMissingQQCredentials) } credentials := &token.QQBotCredentials{ AppID: appID, @@ -238,10 +239,10 @@ func ProbeQQ(_ context.Context, creds map[string]string) error { tokSrc := token.NewQQBotTokenSource(credentials) tok, err := tokSrc.Token() if err != nil { - return fmt.Errorf("%s: %w", errs.ErrQQTokenFetchFailed, err) + return fmt.Errorf("%s: %w", consts.ErrQQTokenFetchFailed, err) } if tok == nil || tok.AccessToken == "" { - return errors.New(errs.ErrQQEmptyToken) + return errors.New(consts.ErrQQEmptyToken) } return nil } @@ -249,31 +250,31 @@ func ProbeQQ(_ context.Context, creds map[string]string) error { // ValidateCredentials checks the admin submitted credentials for a channel type. func ValidateCredentials(t string, creds map[string]string, isUpdate bool) error { switch t { - case model.MessageChannelTypeTelegram: + case consts.MessageChannelTypeTelegram: tok := creds["token"] if strings.TrimSpace(tok) == "" && !isUpdate { - return errors.New(errs.ErrTelegramTokenRequired) + return errors.New(consts.ErrTelegramTokenRequired) } if base, ok := creds["api_base"]; ok && strings.TrimSpace(base) != "" { if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") { - return errors.New(errs.ErrAPIBaseInvalid) + return errors.New(consts.ErrAPIBaseInvalid) } } - case model.MessageChannelTypeQQ: + case consts.MessageChannelTypeQQ: appID := creds["app_id"] secret := creds["client_secret"] if (strings.TrimSpace(appID) == "" || strings.TrimSpace(secret) == "") && !isUpdate { - return errors.New(errs.ErrQQCredentialsRequired) + return errors.New(consts.ErrQQCredentialsRequired) } default: - return errors.New(errs.ErrTypeInvalid) + return errors.New(consts.ErrTypeInvalid) } return nil } // ToDTO projects a channel row onto the admin DTO with credentials masked. -func ToDTO(row *model.MessageChannel, creds, extra map[string]string) model.ChannelDTO { - return model.ChannelDTO{ +func ToDTO(row *entity.MessageChannel, creds, extra map[string]string) do.ChannelDTO { + return do.ChannelDTO{ ID: row.ID, Name: row.Name, Type: row.Type, diff --git a/backend/plugins/domain/message_gateway/service/dispatch.go b/backend/plugins/domain/msg_gateway/service/dispatch.go similarity index 79% rename from backend/plugins/domain/message_gateway/service/dispatch.go rename to backend/plugins/domain/msg_gateway/service/dispatch.go index 9e0c8f78..ff69ec12 100644 --- a/backend/plugins/domain/message_gateway/service/dispatch.go +++ b/backend/plugins/domain/msg_gateway/service/dispatch.go @@ -6,9 +6,10 @@ package service import ( "Wavelet/core/contracts" "Wavelet/pkg/logger" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/repository" + "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" @@ -18,9 +19,9 @@ import ( const ( // TaskDispatchBotMsg is the queue pattern for bot downlink dispatch. - TaskDispatchBotMsg = "message_gateway:dispatch_bot_msg" + TaskDispatchBotMsg = consts.TaskDispatchBotMsg // TaskTypeDispatchBotMsg is the admin type identifier for bot downlink dispatch. - TaskTypeDispatchBotMsg = "dispatch_bot_msg" + TaskTypeDispatchBotMsg = consts.TaskTypeDispatchBotMsg taskQueueDefault = "default" taskParamTypeString = "string" @@ -38,7 +39,7 @@ var BotDispatchMeta = contracts.TaskMetaDTO{ Queue: taskQueueDefault, Retryable: true, Params: []contracts.TaskParamDTO{ - {Name: paramNameText, Label: "消息内容", Type: model.TypeText, Required: true, Placeholder: "要发送的文本", Description: "下发给绑定用户的文本"}, + {Name: paramNameText, Label: "消息内容", Type: consts.TypeText, Required: true, Placeholder: "要发送的文本", Description: "下发给绑定用户的文本"}, {Name: "channel_id", Label: "频道 ID", Type: taskParamTypeString, Required: false, Placeholder: "留空表示全部启用频道", Description: "仅向指定频道的绑定发送"}, {Name: "user_id", Label: "用户 ID", Type: taskParamTypeString, Required: false, Placeholder: "留空表示频道下全部绑定", Description: "仅向指定 Wavelet 用户的绑定发送"}, }, @@ -69,7 +70,7 @@ func (h *BotDispatchHandler) Execute(ctx context.Context, payload []byte) (*cont return nil, err } - channels, err := repository.ListEnabledMessageChannels(ctx) + channels, err := dao.ListEnabledMessageChannels(ctx) if err != nil { return nil, err } @@ -82,7 +83,7 @@ func (h *BotDispatchHandler) Execute(ctx context.Context, payload []byte) (*cont } channels = filtered if len(channels) == 0 { - return nil, errors.New(errs.ErrChannelNotFound) + return nil, errors.New(consts.ErrChannelNotFound) } } @@ -107,20 +108,20 @@ func parseBotDispatchPayload(payload []byte) (botDispatchPayload, error) { var p botDispatchPayload if len(payload) > 0 { if err := json.Unmarshal(payload, &p); err != nil { - return p, fmt.Errorf("%s: %w", errs.ErrInvalidJSONFormat, err) + return p, fmt.Errorf("%s: %w", consts.ErrInvalidJSONFormat, err) } } p.Text = strings.TrimSpace(p.Text) if p.Text == "" { - return p, errors.New(errs.ErrBotDispatchTextRequired) + return p, errors.New(consts.ErrBotDispatchTextRequired) } return p, nil } -func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID uint64, text string) (sent, failed int) { +func dispatchOnChannel(ctx context.Context, row *entity.MessageChannel, userID uint64, text string) (sent, failed int) { factory, ok := Lookup(row.Type) if !ok { - logger.ErrorF(ctx, "bot dispatch: %s type=%s", errs.ErrBotChannelNotRegistered, row.Type) + logger.ErrorF(ctx, "bot dispatch: %s type=%s", consts.ErrBotChannelNotRegistered, row.Type) return 0, 1 } cfg, err := channelConfigFromRow(row) @@ -139,7 +140,7 @@ func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID ui } defer func() { _ = ch.Disconnect(ctx) }() - bindings, err := repository.ListBindingsByChannel(ctx, row.ID) + bindings, err := dao.ListBindingsByChannel(ctx, row.ID) if err != nil { logger.ErrorF(ctx, "bot dispatch: list bindings %d: %v", row.ID, err) return 0, 1 @@ -148,11 +149,11 @@ func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID ui if userID != 0 && bindings[i].UserID != userID { continue } - to := model.Recipient{ + to := do.Recipient{ ChatID: bindings[i].PlatformUserID, PlatformUserID: bindings[i].PlatformUserID, } - if err := ch.Send(ctx, to, model.OutboundMessage{Text: text}); err != nil { + if err := ch.Send(ctx, to, do.OutboundMessage{Text: text}); err != nil { logger.ErrorF(ctx, "bot dispatch: send channel=%d user=%d: %v", row.ID, bindings[i].UserID, err) failed++ continue @@ -162,10 +163,10 @@ func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID ui return sent, failed } -func channelConfigFromRow(row *model.MessageChannel) (model.ChannelConfig, error) { +func channelConfigFromRow(row *entity.MessageChannel) (do.ChannelConfig, error) { creds, err := DecryptCredentials(row.Credentials) if err != nil { - return model.ChannelConfig{}, err + return do.ChannelConfig{}, err } if creds == nil { creds = map[string]string{} @@ -180,7 +181,7 @@ func channelConfigFromRow(row *model.MessageChannel) (model.ChannelConfig, error if extra["base_url"] == "" && creds["api_base"] != "" { extra["base_url"] = creds["api_base"] } - return model.ChannelConfig{ + return do.ChannelConfig{ ID: row.ID, Type: row.Type, Name: row.Name, diff --git a/backend/plugins/domain/message_gateway/service/dispatch_test.go b/backend/plugins/domain/msg_gateway/service/dispatch_test.go similarity index 76% rename from backend/plugins/domain/message_gateway/service/dispatch_test.go rename to backend/plugins/domain/msg_gateway/service/dispatch_test.go index 9ef76d5a..e4ec9850 100644 --- a/backend/plugins/domain/message_gateway/service/dispatch_test.go +++ b/backend/plugins/domain/msg_gateway/service/dispatch_test.go @@ -4,7 +4,9 @@ package service_test import ( - "Wavelet/plugins/domain/message_gateway/service" + "Wavelet/plugins/domain/msg_gateway/dao" + "Wavelet/plugins/domain/msg_gateway/model/entity" + "Wavelet/plugins/domain/msg_gateway/service" "context" "path/filepath" "testing" @@ -13,9 +15,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" - - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/repository" ) type dispatchTestDB struct{ db *gorm.DB } @@ -35,9 +34,9 @@ func TestBotDispatchValidatePayload(t *testing.T) { func TestBotDispatchNoChannels(t *testing.T) { testDB, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "dispatch.db")), &gorm.Config{}) require.NoError(t, err) - require.NoError(t, testDB.AutoMigrate(&model.MessageChannel{}, &model.MessageBinding{})) - repository.SetDBServiceForTest(&dispatchTestDB{db: testDB}) - t.Cleanup(func() { repository.SetDBServiceForTest(nil) }) + require.NoError(t, testDB.AutoMigrate(&entity.MessageChannel{}, &entity.MessageBinding{})) + dao.SetDBServiceForTest(&dispatchTestDB{db: testDB}) + t.Cleanup(func() { dao.SetDBServiceForTest(nil) }) h := &service.BotDispatchHandler{} res, err := h.Execute(context.Background(), []byte(`{"text":"hello"}`)) diff --git a/backend/plugins/domain/message_gateway/service/push.go b/backend/plugins/domain/msg_gateway/service/push.go similarity index 73% rename from backend/plugins/domain/message_gateway/service/push.go rename to backend/plugins/domain/msg_gateway/service/push.go index 38e903b5..9fac30ff 100644 --- a/backend/plugins/domain/message_gateway/service/push.go +++ b/backend/plugins/domain/msg_gateway/service/push.go @@ -7,10 +7,11 @@ import ( "Wavelet/core/contracts" "Wavelet/pkg/logger" "Wavelet/pkg/util" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - pkgpush "Wavelet/plugins/domain/message_gateway/push" - "Wavelet/plugins/domain/message_gateway/repository" + "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" + pkgpush "Wavelet/plugins/domain/msg_gateway/push" "context" "encoding/json" "errors" @@ -24,11 +25,11 @@ import ( var ( builtInEventsMu sync.RWMutex // BuiltInEvents lists all built-in events defined in custom_events. - BuiltInEvents []model.EventMetadata + BuiltInEvents []do.EventMetadata ) // RegisterBuiltInEvent registers a built-in event definition. -func RegisterBuiltInEvent(meta model.EventMetadata) { +func RegisterBuiltInEvent(meta do.EventMetadata) { builtInEventsMu.Lock() defer builtInEventsMu.Unlock() for i, e := range BuiltInEvents { @@ -41,10 +42,10 @@ func RegisterBuiltInEvent(meta model.EventMetadata) { } // GetBuiltInEvents returns a copy of registered built-in events. -func GetBuiltInEvents() []model.EventMetadata { +func GetBuiltInEvents() []do.EventMetadata { builtInEventsMu.RLock() defer builtInEventsMu.RUnlock() - out := make([]model.EventMetadata, len(BuiltInEvents)) + out := make([]do.EventMetadata, len(BuiltInEvents)) copy(out, BuiltInEvents) return out } @@ -62,12 +63,12 @@ func (PushRegistryAdapter) SyncEvents(ctx context.Context) error { return SyncEvents(ctx) } -func eventMetadataFromContract(meta contracts.PushEventMeta) model.EventMetadata { - return model.EventMetadata{ +func eventMetadataFromContract(meta contracts.PushEventMeta) do.EventMetadata { + return do.EventMetadata{ Key: meta.Key, Name: meta.Name, Description: meta.Description, - DefaultTemplate: model.NotificationMessage{ + DefaultTemplate: do.NotificationMessage{ Title: meta.DefaultTemplate.Title, Content: meta.DefaultTemplate.Content, Level: meta.DefaultTemplate.Level, @@ -79,13 +80,13 @@ func eventMetadataFromContract(meta contracts.PushEventMeta) model.EventMetadata // SyncBuiltInEvents seeds a database row for every registered built-in event. func SyncBuiltInEvents(ctx context.Context) error { for _, meta := range GetBuiltInEvents() { - _, err := repository.GetPushEventByKeyRecord(ctx, meta.Key) - if errors.Is(err, errs.ErrRecordNotFound) { + _, 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 := model.PushEvent{ + event := entity.PushEvent{ EventKey: meta.Key, Name: meta.Name, Channels: []string{}, @@ -93,7 +94,7 @@ func SyncBuiltInEvents(ctx context.Context) error { Template: defaultTemplateStr, Enabled: false, } - if err := repository.CreatePushEventRecord(ctx, &event); err != nil { + if err := dao.CreatePushEventRecord(ctx, &event); err != nil { return err } } else if err != nil { @@ -104,23 +105,23 @@ func SyncBuiltInEvents(ctx context.Context) error { } // ListPushEvents lists all configured push events. -func ListPushEvents(ctx context.Context) ([]model.PushEvent, error) { - return repository.ListPushEventsRecord(ctx) +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 model.CreatePushEventRequest) (model.PushEvent, error) { +func CreatePushEvent(ctx context.Context, req do.CreatePushEventRequest) (entity.PushEvent, error) { eventKey, eventName, defaultTemplateBytes, err := GetEventInfo(ctx, req) if err != nil { - return model.PushEvent{}, err + return entity.PushEvent{}, err } - count, err := repository.CountPushEventsByKeyRecord(ctx, eventKey) + count, err := dao.CountPushEventsByKeyRecord(ctx, eventKey) if err != nil { - return model.PushEvent{}, err + return entity.PushEvent{}, err } if count > 0 { - return model.PushEvent{}, errors.New(errs.ErrEventAlreadyConfigured) + return entity.PushEvent{}, errors.New(consts.ErrEventAlreadyConfigured) } templateStr := strings.TrimSpace(req.Template) @@ -129,7 +130,7 @@ func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (mod } else { var tempMap map[string]any if err := json.Unmarshal([]byte(templateStr), &tempMap); err != nil { - return model.PushEvent{}, errors.New(errs.ErrTemplateInvalidJSON) + return entity.PushEvent{}, errors.New(consts.ErrTemplateInvalidJSON) } } @@ -142,7 +143,7 @@ func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (mod targets = []string{} } - event := model.PushEvent{ + event := entity.PushEvent{ EventKey: eventKey, Name: eventName, TaskType: req.TaskType, @@ -152,26 +153,26 @@ func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (mod Enabled: req.Enabled, } if err := event.Validate(); err != nil { - return model.PushEvent{}, err + return entity.PushEvent{}, err } - if err := repository.CreatePushEventRecord(ctx, &event); err != nil { - return model.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 := repository.GetPushEventByIDRecord(ctx, id) + event, err := dao.GetPushEventByIDRecord(ctx, id) if err != nil { return err } - return repository.DeletePushEventRecord(ctx, &event) + return dao.DeletePushEventRecord(ctx, &event) } // UpdatePushEvent replaces mutable push event fields. -func UpdatePushEvent(ctx context.Context, id uint64, req model.UpdatePushEventRequest) error { - event, err := repository.GetPushEventByIDRecord(ctx, id) +func UpdatePushEvent(ctx context.Context, id uint64, req do.UpdatePushEventRequest) error { + event, err := dao.GetPushEventByIDRecord(ctx, id) if err != nil { return err } @@ -183,37 +184,37 @@ func UpdatePushEvent(ctx context.Context, id uint64, req model.UpdatePushEventRe if err := event.Validate(); err != nil { return err } - return repository.SavePushEventRecord(ctx, &event) + 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 := repository.GetPushEventByIDRecord(ctx, id) + 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(errs.ErrEnableWithoutChannels) + return false, errors.New(consts.ErrEnableWithoutChannels) } - if err := repository.UpdatePushEventEnabledRecord(ctx, &event, enabled); err != nil { + if err := dao.UpdatePushEventEnabledRecord(ctx, &event, enabled); err != nil { return false, err } return enabled, nil } // ListPushHistories returns a paginated push delivery audit page. -func ListPushHistories(ctx context.Context, filter model.PushHistoryListFilter) (int64, []model.PushHistory, error) { - return repository.ListPushHistoriesRecord(ctx, filter) +func ListPushHistories(ctx context.Context, filter do.PushHistoryListFilter) (int64, []entity.PushHistory, error) { + return dao.ListPushHistoriesRecord(ctx, filter) } // ApplySMTPFallbackToPushConfig fills an email config from the system SMTP settings. func ApplySMTPFallbackToPushConfig(ctx context.Context, cfg *pkgpush.Config) { - if cfg.Channel != model.ChannelEmail || (cfg.URL != "" && cfg.Key != "") { + if cfg.Channel != consts.ChannelEmail || (cfg.URL != "" && cfg.Key != "") { return } - smtp, err := repository.LoadSMTPConfigRecord(ctx) + smtp, err := dao.LoadSMTPConfigRecord(ctx) if err != nil { logger.ErrorF(ctx, "[Push] 读取 SMTP 系统配置失败: %v", err) return @@ -237,15 +238,15 @@ func RunPushTest(ctx context.Context, cfg pkgpush.Config, target string) error { return err } if err := pusher.ValidateConfig(cfg); err != nil { - return fmt.Errorf("%s: %w", errs.ErrValidationFailed, err) + return fmt.Errorf("%s: %w", consts.ErrValidationFailed, err) } ApplySMTPFallbackToPushConfig(ctx, &cfg) testBody := map[string]any{ - model.KeyTitle: "测试通道推送", - model.KeyContent: "当您收到这条消息,说明当前渠道连通性测试通过。", - model.KeyLevel: model.DefaultLevelInfo, + consts.KeyTitle: "测试通道推送", + consts.KeyContent: "当您收到这条消息,说明当前渠道连通性测试通过。", + consts.KeyLevel: consts.DefaultLevelInfo, } if _, err := pusher.Send(ctx, cfg, target, testBody, "", nil); err != nil { return err @@ -254,21 +255,21 @@ func RunPushTest(ctx context.Context, cfg pkgpush.Config, target string) error { } // ListPushChannels returns every configured push channel. -func ListPushChannels(ctx context.Context) ([]model.PushChannel, error) { - return repository.ListPushChannelsRecord(ctx) +func ListPushChannels(ctx context.Context) ([]entity.PushChannel, error) { + return dao.ListPushChannelsRecord(ctx) } // CreatePushChannel validates uniqueness and persists a new push channel. -func CreatePushChannel(ctx context.Context, req model.CreatePushChannelRequest) (model.PushChannel, error) { - count, err := repository.CountPushChannelsByNameRecord(ctx, req.Name) +func CreatePushChannel(ctx context.Context, req do.CreatePushChannelRequest) (entity.PushChannel, error) { + count, err := dao.CountPushChannelsByNameRecord(ctx, req.Name) if err != nil { - return model.PushChannel{}, err + return entity.PushChannel{}, err } if count > 0 { - return model.PushChannel{}, errors.New(errs.ErrChannelNameExists) + return entity.PushChannel{}, errors.New(consts.ErrChannelNameExists) } - channel := model.PushChannel{ + channel := entity.PushChannel{ Name: req.Name, Description: req.Description, Type: req.Type, @@ -278,19 +279,19 @@ func CreatePushChannel(ctx context.Context, req model.CreatePushChannelRequest) Enabled: req.Enabled, } if err := channel.Validate(); err != nil { - return model.PushChannel{}, err + return entity.PushChannel{}, err } - if err := repository.CreatePushChannelRecord(ctx, &channel); err != nil { - return model.PushChannel{}, err + if err := dao.CreatePushChannelRecord(ctx, &channel); err != nil { + return entity.PushChannel{}, err } return channel, nil } // UpdatePushChannel replaces the mutable fields of an existing push channel. -func UpdatePushChannel(ctx context.Context, id uint64, req model.UpdatePushChannelRequest) (model.PushChannel, error) { - channel, err := repository.GetPushChannelByIDRecord(ctx, id) +func UpdatePushChannel(ctx context.Context, id uint64, req do.UpdatePushChannelRequest) (entity.PushChannel, error) { + channel, err := dao.GetPushChannelByIDRecord(ctx, id) if err != nil { - return model.PushChannel{}, err + return entity.PushChannel{}, err } channel.Description = req.Description @@ -300,30 +301,30 @@ func UpdatePushChannel(ctx context.Context, id uint64, req model.UpdatePushChann channel.Other = req.Other channel.Enabled = req.Enabled if err := channel.Validate(); err != nil { - return model.PushChannel{}, err + return entity.PushChannel{}, err } - if err := repository.SavePushChannelRecord(ctx, &channel); err != nil { - return model.PushChannel{}, err + if err := dao.SavePushChannelRecord(ctx, &channel); err != nil { + return entity.PushChannel{}, err } return channel, nil } // DeletePushChannel removes a push channel by id. func DeletePushChannel(ctx context.Context, id uint64) error { - channel, err := repository.GetPushChannelByIDRecord(ctx, id) + channel, err := dao.GetPushChannelByIDRecord(ctx, id) if err != nil { return err } - return repository.DeletePushChannelRecord(ctx, &channel) + return dao.DeletePushChannelRecord(ctx, &channel) } // LoadChannelForTest resolves the credentials under test, either from a stored // channel name or from the ad-hoc values sent by the caller. -func LoadChannelForTest(ctx context.Context, req model.TestPushChannelRequest) (string, string, string, string, error) { +func LoadChannelForTest(ctx context.Context, req do.TestPushChannelRequest) (string, string, string, string, error) { if req.Name != "" { - channel, err := repository.GetPushChannelByNameRecord(ctx, req.Name) + channel, err := dao.GetPushChannelByNameRecord(ctx, req.Name) if err != nil { - return "", "", "", "", errors.New(errs.ErrChannelNotFound) + return "", "", "", "", errors.New(consts.ErrChannelNotFound) } return channel.URL, channel.Token, channel.Other, channel.Type, nil } @@ -331,17 +332,17 @@ func LoadChannelForTest(ctx context.Context, req model.TestPushChannelRequest) ( } // PreparePushChannelTest builds the connectivity probe payload for a channel. -func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelRequest) (model.SendPayload, error) { +func PreparePushChannelTest(ctx context.Context, req do.TestPushChannelRequest) (do.SendPayload, error) { url, token, other, channelType, err := LoadChannelForTest(ctx, req) if err != nil { - return model.SendPayload{}, err + return do.SendPayload{}, err } - if channelType == model.ChannelEmail { + if channelType == consts.ChannelEmail { url, token, other = ResolveSMTPConfig(ctx, url, token, other) } - tempChannel := model.PushChannel{ + tempChannel := entity.PushChannel{ Name: "test_temp", URL: url, Token: token, @@ -350,23 +351,23 @@ func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelReques Enabled: true, } if err := tempChannel.Validate(); err != nil { - return model.SendPayload{}, err + return do.SendPayload{}, err } url = tempChannel.URL var config pkgpush.Config var renderedJSON string switch channelType { - case model.ChannelLark: - config = pkgpush.Config{Channel: model.ChannelLark, URL: url, Secret: token} + case consts.ChannelLark: + config = pkgpush.Config{Channel: consts.ChannelLark, URL: url, Secret: token} renderedJSON = other - case model.ChannelEmail: - config = pkgpush.Config{Channel: model.ChannelEmail, URL: url, Key: token, Secret: other} - case model.ChannelTelegram: - config = pkgpush.Config{Channel: model.ChannelTelegram, URL: url, Secret: token, Key: other} + case consts.ChannelEmail: + config = pkgpush.Config{Channel: consts.ChannelEmail, URL: url, Key: token, Secret: other} + case consts.ChannelTelegram: + config = pkgpush.Config{Channel: consts.ChannelTelegram, URL: url, Secret: token, Key: other} default: - config = pkgpush.Config{Channel: model.ChannelCustom, URL: url} - customPushReq := model.CustomPushRequest{ + config = pkgpush.Config{Channel: consts.ChannelCustom, URL: url} + customPushReq := do.CustomPushRequest{ Title: "通道测试通知", Content: "这是一条来自系统的消息通道连通性测试消息。", Description: "系统通道测试", @@ -376,14 +377,14 @@ func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelReques renderedJSON = RenderCustomPayload(other, customPushReq) } - return model.SendPayload{ + return do.SendPayload{ EventKey: "test_channel", Config: config, Target: req.Target, - Body: model.NotificationMessage{ + Body: do.NotificationMessage{ Title: "通道测试通知", Content: "这是一条来自系统的消息通道连通性测试消息。", - Level: model.DefaultLevelInfo, + Level: consts.DefaultLevelInfo, }, Template: renderedJSON, }, nil @@ -391,7 +392,7 @@ func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelReques // RenderCustomPayload substitutes the supported template variables of a custom // webhook body, JSON-escaping every injected value. -func RenderCustomPayload(template string, req model.CustomPushRequest) string { +func RenderCustomPayload(template string, req do.CustomPushRequest) string { result := template result = strings.ReplaceAll(result, "$title", EscapeJSONString(req.Title)) result = strings.ReplaceAll(result, "$description", EscapeJSONString(req.Description)) @@ -412,20 +413,20 @@ func EscapeJSONString(s string) string { } // ListActivePushEventsByTaskType returns enabled push events for a given task type. -func ListActivePushEventsByTaskType(ctx context.Context, taskType string) ([]model.PushEvent, error) { - return repository.ListActivePushEventsByTaskTypeRecord(ctx, taskType) +func ListActivePushEventsByTaskType(ctx context.Context, taskType string) ([]entity.PushEvent, error) { + return dao.ListActivePushEventsByTaskTypeRecord(ctx, taskType) } // QueryUser resolves a user through the UserService contract, falling back to the -// repository read path while the contract is not wired yet. +// DAO read path while the contract is not wired yet. func QueryUser(ctx context.Context, fromService func(contracts.UserService) (*contracts.UserDTO, error), dbField string, dbVal any) (*contracts.UserDTO, error) { if userSvc := GetUserService(ctx); userSvc != nil { return fromService(userSvc) } - if user, err := repository.FindUserByFieldRecord(ctx, dbField, dbVal); err == nil && user != nil { + if user, err := dao.FindUserByFieldRecord(ctx, dbField, dbVal); err == nil && user != nil { return user, nil } - return nil, errors.New(errs.ErrUserNotFound) + return nil, errors.New(consts.ErrUserNotFound) } // FindUserByID resolves a user by primary key. @@ -463,7 +464,7 @@ func LoadUserFromPayload(ctx context.Context, data map[string]any) any { } // RecordPushHistory creates a push history audit record. -func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMsg string) error { +func RecordPushHistory(ctx context.Context, req do.SendPayload, status, errMsg string) error { title := req.Body.Title content := req.Body.Content level := req.Body.Level @@ -471,7 +472,7 @@ func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMs title = "系统通知" } if level == "" { - level = model.DefaultLevelInfo + level = consts.DefaultLevelInfo } target := req.Target @@ -488,7 +489,7 @@ func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMs } } - history := model.PushHistory{ + history := entity.PushHistory{ EventKey: req.EventKey, Channel: req.Config.Channel, Target: target, @@ -498,7 +499,7 @@ func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMs Status: status, ErrorMsg: errMsg, } - return repository.CreatePushHistoryRecord(ctx, &history) + return dao.CreatePushHistoryRecord(ctx, &history) } // ResolveTarget parses dynamic placeholders into concrete receiver targets. @@ -520,10 +521,10 @@ func ResolveTarget(ctx context.Context, target string, flatBody map[string]any, if !found { return resolved } - if channel == model.ChannelEmail && user.Email != "" { + if channel == consts.ChannelEmail && user.Email != "" { return user.Email } - if channel != model.ChannelEmail && user.Username != "" { + if channel != consts.ChannelEmail && user.Username != "" { return user.Username } return resolved @@ -546,7 +547,7 @@ func ResolveDynamicKeyword(target string, flatBody map[string]any) string { if val, ok := flatBody["username"]; ok { return fmt.Sprintf("%v", val) } - case "user.email", model.ChannelEmail: + case "user.email", consts.ChannelEmail: if val, ok := flatBody["user.email"]; ok { return fmt.Sprintf("%v", val) } @@ -571,15 +572,15 @@ func ResolveTargetUser(ctx context.Context, resolved, _ string) (contracts.UserD } // GetFirstAdminUser resolves the first administrator through the UserService -// contract, falling back to the repository read path when it is unavailable. +// contract, falling back to the DAO read path when it is unavailable. func GetFirstAdminUser(ctx context.Context) (*contracts.UserDTO, error) { if userSvc := GetUserService(ctx); userSvc != nil { return userSvc.GetFirstAdminUser(ctx) } - if adminUser, err := repository.FindFirstAdminUserRecord(ctx); err == nil && adminUser != nil { + if adminUser, err := dao.FindFirstAdminUserRecord(ctx); err == nil && adminUser != nil { return adminUser, nil } - return nil, errors.New(errs.ErrNoAdminUser) + return nil, errors.New(consts.ErrNoAdminUser) } // ResolveSystemTarget maps system receiver aliases to administrator contact info. @@ -591,10 +592,10 @@ func ResolveSystemTarget(ctx context.Context, resolved, channel string) (string, if err != nil || adminUser == nil { return resolved, true } - if channel == model.ChannelEmail && adminUser.Email != "" { + if channel == consts.ChannelEmail && adminUser.Email != "" { return adminUser.Email, true } - if channel != model.ChannelEmail && adminUser.Username != "" { + if channel != consts.ChannelEmail && adminUser.Username != "" { return adminUser.Username, true } return resolved, true @@ -605,7 +606,7 @@ func ResolveSMTPConfig(ctx context.Context, url, token, other string) (string, s if url != "" && token != "" { return url, token, other } - smtp, err := repository.LoadSMTPConfigRecord(ctx) + smtp, err := dao.LoadSMTPConfigRecord(ctx) if err != nil { logger.ErrorF(ctx, "[Push] 读取 SMTP 系统配置失败: %v", err) return url, token, other @@ -641,18 +642,18 @@ func GetSystemUser(ctx context.Context) *contracts.UserDTO { } // FindBuiltInEvent finds a registered built-in event by key. -func FindBuiltInEvent(key string) (model.EventMetadata, bool) { +func FindBuiltInEvent(key string) (do.EventMetadata, bool) { for _, meta := range GetBuiltInEvents() { if meta.Key == key { return meta, true } } - return model.EventMetadata{}, false + return do.EventMetadata{}, false } // 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 model.CreatePushEventRequest) (string, string, []byte, error) { +func GetEventInfo(ctx context.Context, req do.CreatePushEventRequest) (string, string, []byte, error) { if req.TaskType != "" { taskName := req.TaskType if taskSvc := GetTaskService(ctx); taskSvc != nil { @@ -662,10 +663,10 @@ func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string } eventKey := "task_completed:" + req.TaskType eventName := "任务完成: " + taskName - defaultTemplate := model.NotificationMessage{ + defaultTemplate := do.NotificationMessage{ Title: "任务完成: " + taskName, Content: "异步任务 {{task_name}} (ID: {{task_id}}) 已完成。状态: {{task_status}},耗时: {{task_duration}} ms。", - Level: model.DefaultLevelInfo, + Level: consts.DefaultLevelInfo, } defaultTemplateBytes, err := json.Marshal(defaultTemplate) if err != nil { @@ -675,12 +676,12 @@ func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string } if req.EventKey == "" { - return "", "", nil, errors.New(errs.ErrEventKeyOrTaskType) + return "", "", nil, errors.New(consts.ErrEventKeyOrTaskType) } meta, found := FindBuiltInEvent(req.EventKey) if !found { - return "", "", nil, errors.New(errs.ErrUnsupportedEventKey) + return "", "", nil, errors.New(consts.ErrUnsupportedEventKey) } defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate) @@ -691,7 +692,7 @@ func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string } // EnqueuePushTask dispatches a notification payload to the async push worker. -func EnqueuePushTask(ctx context.Context, payload model.SendPayload) error { +func EnqueuePushTask(ctx context.Context, payload do.SendPayload) error { payloadBytes, err := json.Marshal(payload) if err != nil { return err @@ -700,7 +701,7 @@ func EnqueuePushTask(ctx context.Context, payload model.SendPayload) error { _, err = taskSvc.Dispatch(ctx, "send_notification", payloadBytes, contracts.TaskTriggerSystem) return err } - return errors.New(errs.ErrTaskServiceUnavailable) + return errors.New(consts.ErrTaskServiceUnavailable) } // GetFlatBody flattens nested body map. @@ -736,9 +737,9 @@ func FlattenMap(prefix string, m, result map[string]any) { const ( // SendNotificationTask is the asynq task name for push notification. - SendNotificationTask = "push:send" + SendNotificationTask = consts.SendNotificationTask // TaskTypeSendNotification is the admin task manager type identifier. - TaskTypeSendNotification = "send_notification" + TaskTypeSendNotification = consts.TaskTypeSendNotification ) // SendNotificationMeta represents the task metadata. @@ -778,16 +779,16 @@ type PushHandler struct{} // ValidatePayload validates and normalizes push parameters. func (h *PushHandler) ValidatePayload(payload []byte) ([]byte, error) { if len(payload) == 0 { - return nil, errors.New(errs.ErrPayloadRequired) + return nil, errors.New(consts.ErrPayloadRequired) } - var req model.SendPayload + var req do.SendPayload if err := json.Unmarshal(payload, &req); err != nil { - return nil, fmt.Errorf("%s: %w", errs.ErrInvalidJSONFormat, err) + return nil, fmt.Errorf("%s: %w", consts.ErrInvalidJSONFormat, err) } if req.Config.Channel == "" { - return nil, errors.New(errs.ErrChannelTypeRequired) + return nil, errors.New(consts.ErrChannelTypeRequired) } return json.Marshal(req) @@ -795,17 +796,17 @@ func (h *PushHandler) ValidatePayload(payload []byte) ([]byte, error) { // Execute performs the push send and logs delivery history audit. func (h *PushHandler) Execute(ctx context.Context, payload []byte) error { - var req model.SendPayload + var req do.SendPayload if err := json.Unmarshal(payload, &req); err != nil { logger.ErrorF(ctx, "[Push] 解析推送参数失败: %v", err) - return fmt.Errorf("%s: %w", errs.ErrParsePayloadFailed, err) + return fmt.Errorf("%s: %w", consts.ErrParsePayloadFailed, err) } logger.InfoF(ctx, "[Push] 开始推送通知: 事件 = %s, 渠道 = %s, 接收目标 = %s", req.EventKey, req.Config.Channel, req.Target) pusher, err := pkgpush.GetPusher(req.Config.Channel) if err != nil { - errWrap := fmt.Errorf("%s: %w", errs.ErrGetPusherFailed, err) + errWrap := fmt.Errorf("%s: %w", consts.ErrGetPusherFailed, err) logger.ErrorF(ctx, "[Push] 推送失败: %v", errWrap) h.recordHistory(ctx, req, "failed", errWrap.Error()) return errWrap @@ -829,7 +830,7 @@ func (h *PushHandler) Execute(ctx context.Context, payload []byte) error { return nil } -func (h *PushHandler) recordHistory(ctx context.Context, req model.SendPayload, status, errMsg string) { +func (h *PushHandler) recordHistory(ctx context.Context, req do.SendPayload, status, errMsg string) { if dbErr := RecordPushHistory(ctx, req, status, errMsg); dbErr != nil { logger.ErrorF(ctx, "[Push] 写入推送历史审计记录失败: %v", dbErr) } @@ -873,7 +874,7 @@ func HandleTaskCompleted(ctx context.Context, e contracts.TaskCompletedEvent) { } for _, event := range events { - meta := model.EventMetadata{ + meta := do.EventMetadata{ Key: event.EventKey, Name: event.Name, Description: "异步任务执行完毕触发的自动通知", @@ -942,7 +943,7 @@ type EventTrigger struct{} var DefaultTrigger = &EventTrigger{} // Trigger receives event metadata and processes the event notification dispatch asynchronously. -func (t *EventTrigger) Trigger(ctx context.Context, meta model.EventMetadata, body map[string]any) { +func (t *EventTrigger) Trigger(ctx context.Context, meta do.EventMetadata, body map[string]any) { asyncCtx := context.WithoutCancel(ctx) util.Go(func() { if body == nil { @@ -952,9 +953,9 @@ func (t *EventTrigger) Trigger(ctx context.Context, meta model.EventMetadata, bo body["user"] = GetSystemUser(asyncCtx) } - eventPtr, err := repository.GetActivePushEventByKey(asyncCtx, meta.Key) + eventPtr, err := dao.GetActivePushEventByKey(asyncCtx, meta.Key) if err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { + if errors.Is(err, consts.ErrRecordNotFound) { return } logger.ErrorF(asyncCtx, "push_event_trigger: failed to get active event %s: %v", meta.Key, err) @@ -971,8 +972,8 @@ func (t *EventTrigger) Trigger(ctx context.Context, meta model.EventMetadata, bo }) } -func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMetadata, flatBody, body map[string]any) (model.NotificationMessage, string) { - var msg model.NotificationMessage +func (t *EventTrigger) buildMessage(event *entity.PushEvent, meta do.EventMetadata, flatBody, body map[string]any) (do.NotificationMessage, string) { + var msg do.NotificationMessage renderedTemplate := "" templateSource := event.Template @@ -982,7 +983,7 @@ func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMeta if err != nil { msg.Title = event.Name msg.Content = renderedTemplate - msg.Level = model.DefaultLevelInfo + msg.Level = consts.DefaultLevelInfo } } else { msg = t.parseDefaultTemplate(meta, flatBody) @@ -992,7 +993,7 @@ func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMeta msg.Ext = make(map[string]any) } for k, v := range body { - if k == model.KeyTitle || k == model.KeyContent || k == model.KeyLevel { + if k == consts.KeyTitle || k == consts.KeyContent || k == consts.KeyLevel { continue } if _, exists := msg.Ext[k]; !exists { @@ -1003,8 +1004,8 @@ func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMeta return msg, renderedTemplate } -func (t *EventTrigger) parseCustomTemplate(event *model.PushEvent, templateSource string, flatBody map[string]any) (model.NotificationMessage, string, error) { - var msg model.NotificationMessage +func (t *EventTrigger) parseCustomTemplate(event *entity.PushEvent, templateSource string, flatBody map[string]any) (do.NotificationMessage, string, error) { + var msg do.NotificationMessage renderedTemplate := pkgpush.ParseTemplate(templateSource, flatBody) var tMap map[string]any @@ -1012,33 +1013,33 @@ func (t *EventTrigger) parseCustomTemplate(event *model.PushEvent, templateSourc return msg, renderedTemplate, err } - if title, ok := tMap[model.KeyTitle].(string); ok && title != "" { + if title, ok := tMap[consts.KeyTitle].(string); ok && title != "" { msg.Title = title } else { msg.Title = event.Name } - delete(tMap, model.KeyTitle) + delete(tMap, consts.KeyTitle) - if content, ok := tMap[model.KeyContent].(string); ok && content != "" { + if content, ok := tMap[consts.KeyContent].(string); ok && content != "" { msg.Content = content } else { msg.Content = renderedTemplate } - delete(tMap, model.KeyContent) + delete(tMap, consts.KeyContent) - if level, ok := tMap[model.KeyLevel].(string); ok && level != "" { + if level, ok := tMap[consts.KeyLevel].(string); ok && level != "" { msg.Level = level } else { - msg.Level = model.DefaultLevelInfo + msg.Level = consts.DefaultLevelInfo } - delete(tMap, model.KeyLevel) + delete(tMap, consts.KeyLevel) msg.Ext = tMap return msg, renderedTemplate, nil } -func (t *EventTrigger) parseDefaultTemplate(meta model.EventMetadata, flatBody map[string]any) model.NotificationMessage { - var msg model.NotificationMessage +func (t *EventTrigger) parseDefaultTemplate(meta do.EventMetadata, flatBody map[string]any) do.NotificationMessage { + var msg do.NotificationMessage msg.Title = pkgpush.ParseTemplate(meta.DefaultTemplate.Title, flatBody) msg.Content = pkgpush.ParseTemplate(meta.DefaultTemplate.Content, flatBody) msg.Level = pkgpush.ParseTemplate(meta.DefaultTemplate.Level, flatBody) @@ -1056,9 +1057,9 @@ func (t *EventTrigger) parseDefaultTemplate(meta model.EventMetadata, flatBody m return msg } -func (t *EventTrigger) enqueuePushTasks(ctx context.Context, meta model.EventMetadata, event *model.PushEvent, msg model.NotificationMessage, flatBody map[string]any) { +func (t *EventTrigger) enqueuePushTasks(ctx context.Context, meta do.EventMetadata, event *entity.PushEvent, msg do.NotificationMessage, flatBody map[string]any) { for _, channelName := range event.Channels { - customChannel, err := repository.GetActivePushChannelByName(ctx, channelName) + customChannel, err := dao.GetActivePushChannelByName(ctx, channelName) if err == nil { t.enqueueCustomPushChannelTasks(ctx, meta, event, customChannel, msg, flatBody) continue @@ -1067,7 +1068,7 @@ func (t *EventTrigger) enqueuePushTasks(ctx context.Context, meta model.EventMet } } -func (t *EventTrigger) enqueueCustomPushChannelTasks(ctx context.Context, meta model.EventMetadata, event *model.PushEvent, channel *model.PushChannel, msg model.NotificationMessage, flatBody map[string]any) { +func (t *EventTrigger) enqueueCustomPushChannelTasks(ctx context.Context, meta do.EventMetadata, event *entity.PushEvent, channel *entity.PushChannel, msg do.NotificationMessage, flatBody map[string]any) { if len(event.Targets) == 0 { t.enqueueSingleCustomPushChannelTask(ctx, meta, channel, "", msg) return @@ -1079,22 +1080,22 @@ func (t *EventTrigger) enqueueCustomPushChannelTasks(ctx context.Context, meta m } } -func (t *EventTrigger) enqueueSingleCustomPushChannelTask(ctx context.Context, meta model.EventMetadata, channel *model.PushChannel, target string, msg model.NotificationMessage) { +func (t *EventTrigger) enqueueSingleCustomPushChannelTask(ctx context.Context, meta do.EventMetadata, channel *entity.PushChannel, target string, msg do.NotificationMessage) { var config pkgpush.Config var renderedTemplate string switch channel.Type { - case model.ChannelLark: - config = pkgpush.Config{Channel: model.ChannelLark, URL: channel.URL, Secret: channel.Token} + case consts.ChannelLark: + config = pkgpush.Config{Channel: consts.ChannelLark, URL: channel.URL, Secret: channel.Token} renderedTemplate = channel.Other - case model.ChannelEmail: + case consts.ChannelEmail: url, token, other := ResolveSMTPConfig(ctx, channel.URL, channel.Token, channel.Other) - config = pkgpush.Config{Channel: model.ChannelEmail, URL: url, Key: token, Secret: other} - case model.ChannelTelegram: - config = pkgpush.Config{Channel: model.ChannelTelegram, URL: channel.URL, Secret: channel.Token, Key: channel.Other} + config = pkgpush.Config{Channel: consts.ChannelEmail, URL: url, Key: token, Secret: other} + case consts.ChannelTelegram: + config = pkgpush.Config{Channel: consts.ChannelTelegram, URL: channel.URL, Secret: channel.Token, Key: channel.Other} default: - config = pkgpush.Config{Channel: model.ChannelCustom, URL: channel.URL} - customPushReq := model.CustomPushRequest{ + config = pkgpush.Config{Channel: consts.ChannelCustom, URL: channel.URL} + customPushReq := do.CustomPushRequest{ Title: msg.Title, Content: msg.Content, Description: meta.Description, @@ -1106,7 +1107,7 @@ func (t *EventTrigger) enqueueSingleCustomPushChannelTask(ctx context.Context, m renderedTemplate = RenderCustomPayload(channel.Other, customPushReq) } - payload := model.SendPayload{ + payload := do.SendPayload{ EventKey: meta.Key, Config: config, Target: target, @@ -1124,10 +1125,10 @@ func SyncEvents(ctx context.Context) error { } // AdminLogin is the metadata definition for the admin login event. -var AdminLogin = model.EventMetadata{ +var AdminLogin = do.EventMetadata{ Key: "admin_login", Name: "管理员登录", - DefaultTemplate: model.NotificationMessage{ + DefaultTemplate: do.NotificationMessage{ Title: "管理员登录提醒", Content: "管理员 {{user.username}} 于 {{time}} 从 IP {{ip}} 登录系统。", Level: "INFO", diff --git a/backend/plugins/domain/message_gateway/service/service.go b/backend/plugins/domain/msg_gateway/service/service.go similarity index 73% rename from backend/plugins/domain/message_gateway/service/service.go rename to backend/plugins/domain/msg_gateway/service/service.go index b298c5ec..82236e1f 100644 --- a/backend/plugins/domain/message_gateway/service/service.go +++ b/backend/plugins/domain/msg_gateway/service/service.go @@ -1,7 +1,7 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -// Package service implements domain business logic and channel runners for message_gateway. +// Package service implements domain business logic and channel runners for msg_gateway. package service import ( @@ -9,9 +9,10 @@ import ( "Wavelet/core/contracts" "Wavelet/pkg/logger" "Wavelet/pkg/util" - "Wavelet/plugins/domain/message_gateway/errs" - "Wavelet/plugins/domain/message_gateway/model" - "Wavelet/plugins/domain/message_gateway/repository" + "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" "crypto/rand" "crypto/sha256" @@ -26,18 +27,18 @@ import ( ) // Handler processes one inbound message. -type Handler func(ctx context.Context, msg model.InboundMessage) error +type Handler func(ctx context.Context, msg do.InboundMessage) error // Factory constructs a Channel from decrypted config. -type Factory func(cfg model.ChannelConfig, onInbound Handler) (Channel, error) +type Factory func(cfg do.ChannelConfig, onInbound Handler) (Channel, error) // Channel is one connected messaging adapter. type Channel interface { Type() string Connect(ctx context.Context) error Disconnect(ctx context.Context) error - Send(ctx context.Context, to model.Recipient, msg model.OutboundMessage) error - Capabilities() model.Capability + Send(ctx context.Context, to do.Recipient, msg do.OutboundMessage) error + Capabilities() do.Capability } var ( @@ -60,11 +61,11 @@ func Lookup(typ string) (Factory, bool) { return fn, ok } -// CodeAlphabet excludes easily confused runes 0/O/1/I. -const CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" - -// CodeLength is the raw pairing code size. -const CodeLength = 8 +// Re-exported constants. +const ( + CodeAlphabet = consts.CodeAlphabet + CodeLength = consts.CodeLength +) // GenerateCode returns an 8-character pairing code. func GenerateCode() (string, error) { @@ -282,87 +283,87 @@ func GetUserService(ctx context.Context) contracts.UserService { } // BindChannel consumes a pairing code and binds the platform identity to the user. -func BindChannel(ctx context.Context, userID uint64, req model.BindRequest) (model.BindingDTO, error) { +func BindChannel(ctx context.Context, userID uint64, req do.BindRequest) (do.BindingDTO, error) { channelID, err := strconv.ParseUint(strings.TrimSpace(req.ChannelID), 10, 64) if err != nil || channelID == 0 { - return model.BindingDTO{}, errs.ErrChannelIDRequired + return do.BindingDTO{}, consts.ErrChannelIDRequired } code := NormalizeCode(req.Code) if code == "" { - return model.BindingDTO{}, errs.ErrCodeInvalid + return do.BindingDTO{}, consts.ErrCodeInvalid } - pairing, err := repository.GetPairingCode(ctx, code) + pairing, err := dao.GetPairingCode(ctx, code) if err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { - return model.BindingDTO{}, errs.ErrCodeInvalid + if errors.Is(err, consts.ErrRecordNotFound) { + return do.BindingDTO{}, consts.ErrCodeInvalid } - return model.BindingDTO{}, err + return do.BindingDTO{}, err } if !pairing.ExpiresAt.After(time.Now()) { - return model.BindingDTO{}, errs.ErrCodeInvalid + return do.BindingDTO{}, consts.ErrCodeInvalid } if pairing.ChannelID != channelID { - return model.BindingDTO{}, errs.ErrChannelMismatch + return do.BindingDTO{}, consts.ErrChannelMismatch } - ch, err := repository.GetMessageChannel(ctx, channelID) + ch, err := dao.GetMessageChannel(ctx, channelID) if err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { - return model.BindingDTO{}, errs.ErrCodeInvalid + if errors.Is(err, consts.ErrRecordNotFound) { + return do.BindingDTO{}, consts.ErrCodeInvalid } - return model.BindingDTO{}, err + return do.BindingDTO{}, err } if !ch.Enabled { - return model.BindingDTO{}, errs.ErrChannelDisabled + return do.BindingDTO{}, consts.ErrChannelDisabled } - existing, err := repository.GetBindingByChannelPlatform(ctx, channelID, pairing.PlatformUserID) - if err != nil && !errors.Is(err, errs.ErrRecordNotFound) { - return model.BindingDTO{}, err + existing, err := dao.GetBindingByChannelPlatform(ctx, channelID, pairing.PlatformUserID) + if err != nil && !errors.Is(err, consts.ErrRecordNotFound) { + return do.BindingDTO{}, err } if err == nil && existing != nil { if existing.UserID != userID { - return model.BindingDTO{}, errs.ErrPlatformAlreadyBound + return do.BindingDTO{}, consts.ErrPlatformAlreadyBound } - _ = repository.DeletePairingCode(ctx, pairing.Code) + _ = dao.DeletePairingCode(ctx, pairing.Code) return ToBindingDTO(existing, ch), nil } - row := &model.MessageBinding{ + row := &entity.MessageBinding{ UserID: userID, ChannelID: channelID, PlatformUserID: pairing.PlatformUserID, } - if err := repository.CreateMessageBinding(ctx, row); err != nil { - return model.BindingDTO{}, err + if err := dao.CreateMessageBinding(ctx, row); err != nil { + return do.BindingDTO{}, err } - if err := repository.DeletePairingCode(ctx, pairing.Code); err != nil { - return model.BindingDTO{}, err + if err := dao.DeletePairingCode(ctx, pairing.Code); err != nil { + return do.BindingDTO{}, err } return ToBindingDTO(row, ch), nil } // ListEnabledPublicChannels returns the channels a user may bind to. -func ListEnabledPublicChannels(ctx context.Context) ([]model.PublicChannelDTO, error) { - rows, err := repository.ListEnabledMessageChannels(ctx) +func ListEnabledPublicChannels(ctx context.Context) ([]do.PublicChannelDTO, error) { + rows, err := dao.ListEnabledMessageChannels(ctx) if err != nil { return nil, err } - out := make([]model.PublicChannelDTO, 0, len(rows)) + out := make([]do.PublicChannelDTO, 0, len(rows)) for _, row := range rows { - out = append(out, model.PublicChannelDTO{ID: row.ID, Name: row.Name, Type: row.Type}) + out = append(out, do.PublicChannelDTO{ID: row.ID, Name: row.Name, Type: row.Type}) } return out, nil } // ListUserBindings returns the binding rows of one user enriched with channel info. -func ListUserBindings(ctx context.Context, userID uint64) ([]model.BindingDTO, error) { - rows, err := repository.ListBindingsByUser(ctx, userID) +func ListUserBindings(ctx context.Context, userID uint64) ([]do.BindingDTO, error) { + rows, err := dao.ListBindingsByUser(ctx, userID) if err != nil { return nil, err } - out := make([]model.BindingDTO, 0, len(rows)) + out := make([]do.BindingDTO, 0, len(rows)) for i := range rows { - ch, err := repository.GetMessageChannel(ctx, rows[i].ChannelID) + ch, err := dao.GetMessageChannel(ctx, rows[i].ChannelID) if err != nil { out = append(out, ToBindingDTO(&rows[i], nil)) continue @@ -374,22 +375,22 @@ func ListUserBindings(ctx context.Context, userID uint64) ([]model.BindingDTO, e // UnbindChannel removes a binding owned by the given user. func UnbindChannel(ctx context.Context, userID, bindingID uint64) error { - row, err := repository.GetMessageBinding(ctx, bindingID) + row, err := dao.GetMessageBinding(ctx, bindingID) if err != nil { - if errors.Is(err, errs.ErrRecordNotFound) { - return errs.ErrBindingNotFound + if errors.Is(err, consts.ErrRecordNotFound) { + return consts.ErrBindingNotFound } return err } if row.UserID != userID { - return errs.ErrBindingForbidden + return consts.ErrBindingForbidden } - return repository.DeleteMessageBinding(ctx, bindingID) + return dao.DeleteMessageBinding(ctx, bindingID) } // ToBindingDTO projects a binding row and its optional channel onto the user DTO. -func ToBindingDTO(row *model.MessageBinding, ch *model.MessageChannel) model.BindingDTO { - dto := model.BindingDTO{ +func ToBindingDTO(row *entity.MessageBinding, ch *entity.MessageChannel) do.BindingDTO { + dto := do.BindingDTO{ ID: row.ID, UserID: row.UserID, ChannelID: row.ChannelID, diff --git a/backend/plugins/domain/system/plugin.go b/backend/plugins/domain/system/plugin.go index 9d9be0d5..1a45bcc9 100644 --- a/backend/plugins/domain/system/plugin.go +++ b/backend/plugins/domain/system/plugin.go @@ -66,11 +66,6 @@ func (p *Plugin) Apply(ctx *core.Context) error { }) ctx.Router().RegisterWhitelist("/api/v1/config/public") - // 3. Custom injection - ctx.Router().GET("/custom", func(c *gin.Context) { - c.JSON(http.StatusOK, response.OK(gin.H{"custom": true})) - }) - return nil }