From cb018b3b60d942cce98b3979f064136986843f44 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 14 Jun 2026 16:52:06 +0800 Subject: [PATCH] feat(push): implement system notification and push framework --- .agent/skills/push-notification/SKILL.md | 176 +++ AGENTS.md | 16 + Makefile | 14 + docs/docs.go | 1049 +++++++++++++++++ docs/swagger.json | 1049 +++++++++++++++++ docs/swagger.yaml | 646 ++++++++++ frontend/app/(main)/admin/push/page.tsx | 14 + .../common/admin/push-notification.tsx | 65 + .../common/admin/push/events-tab.tsx | 609 ++++++++++ .../common/admin/push/histories-tab.tsx | 192 +++ .../common/admin/push/settings-tab.tsx | 631 ++++++++++ frontend/components/layout/sidebar.tsx | 2 + frontend/lib/services/index.ts | 7 + frontend/lib/services/push/index.ts | 5 + frontend/lib/services/push/push.service.ts | 123 ++ frontend/lib/services/push/types.ts | 136 +++ internal/apps/admin/push/channels.go | 363 ++++++ .../apps/admin/push/channels_definition.go | 150 +++ internal/apps/admin/push/constants.go | 14 + .../admin/push/custom_events/admin_login.go | 43 + internal/apps/admin/push/events.go | 599 ++++++++++ internal/apps/admin/push/push_test.go | 737 ++++++++++++ internal/apps/admin/push/routers.go | 450 +++++++ internal/apps/admin/push/tasks.go | 163 +++ internal/apps/admin/task/routers_test.go | 37 +- internal/apps/oauth/middlewares.go | 36 +- internal/apps/oauth/oauth_test.go | 74 ++ internal/apps/oauth/sources.go | 3 + internal/apps/upload/file_management.go | 6 +- internal/apps/upload/routers.go | 4 +- internal/apps/upload/storage_ops.go | 4 +- internal/apps/upload/tasks.go | 97 +- internal/apps/upload/tasks_test.go | 59 +- internal/apps/user/routers.go | 3 + .../202606140001_create_push_tables.sql | 34 + .../202606140002_add_push_configs.sql | 27 + .../postgres/202606140003_add_system_user.sql | 8 + .../202606140004_create_push_channels.sql | 31 + ...6140005_update_system_cleanup_schedule.sql | 11 + .../202606140001_create_push_tables.sql | 34 + .../sqlite/202606140002_add_push_configs.sql | 27 + .../sqlite/202606140003_add_system_user.sql | 7 + .../202606140004_create_push_channels.sql | 31 + ...6140005_update_system_cleanup_schedule.sql | 11 + internal/db/migrator/migrator_test.go | 2 +- internal/model/push_channel.go | 104 ++ internal/model/push_event.go | 59 + internal/model/push_history.go | 27 + internal/model/system_configs.go | 1 + internal/model/task_execution_test.go | 24 +- internal/router/router.go | 26 + internal/service/cleanup.go | 149 +++ internal/task/executor.go | 39 +- internal/task/handlers/register.go | 12 +- pkg/push/custom.go | 80 ++ pkg/push/email.go | 109 ++ pkg/push/lark.go | 273 +++++ pkg/push/push.go | 61 + pkg/push/template.go | 78 ++ pkg/push/template_test.go | 75 ++ 60 files changed, 8756 insertions(+), 160 deletions(-) create mode 100644 .agent/skills/push-notification/SKILL.md create mode 100644 frontend/app/(main)/admin/push/page.tsx create mode 100644 frontend/components/common/admin/push-notification.tsx create mode 100644 frontend/components/common/admin/push/events-tab.tsx create mode 100644 frontend/components/common/admin/push/histories-tab.tsx create mode 100644 frontend/components/common/admin/push/settings-tab.tsx create mode 100644 frontend/lib/services/push/index.ts create mode 100644 frontend/lib/services/push/push.service.ts create mode 100644 frontend/lib/services/push/types.ts create mode 100644 internal/apps/admin/push/channels.go create mode 100644 internal/apps/admin/push/channels_definition.go create mode 100644 internal/apps/admin/push/constants.go create mode 100644 internal/apps/admin/push/custom_events/admin_login.go create mode 100644 internal/apps/admin/push/events.go create mode 100644 internal/apps/admin/push/push_test.go create mode 100644 internal/apps/admin/push/routers.go create mode 100644 internal/apps/admin/push/tasks.go create mode 100644 internal/db/migrator/goose/postgres/202606140001_create_push_tables.sql create mode 100644 internal/db/migrator/goose/postgres/202606140002_add_push_configs.sql create mode 100644 internal/db/migrator/goose/postgres/202606140003_add_system_user.sql create mode 100644 internal/db/migrator/goose/postgres/202606140004_create_push_channels.sql create mode 100644 internal/db/migrator/goose/postgres/202606140005_update_system_cleanup_schedule.sql create mode 100644 internal/db/migrator/goose/sqlite/202606140001_create_push_tables.sql create mode 100644 internal/db/migrator/goose/sqlite/202606140002_add_push_configs.sql create mode 100644 internal/db/migrator/goose/sqlite/202606140003_add_system_user.sql create mode 100644 internal/db/migrator/goose/sqlite/202606140004_create_push_channels.sql create mode 100644 internal/db/migrator/goose/sqlite/202606140005_update_system_cleanup_schedule.sql create mode 100644 internal/model/push_channel.go create mode 100644 internal/model/push_event.go create mode 100644 internal/model/push_history.go create mode 100644 internal/service/cleanup.go create mode 100644 pkg/push/custom.go create mode 100644 pkg/push/email.go create mode 100644 pkg/push/lark.go create mode 100644 pkg/push/push.go create mode 100644 pkg/push/template.go create mode 100644 pkg/push/template_test.go diff --git a/.agent/skills/push-notification/SKILL.md b/.agent/skills/push-notification/SKILL.md new file mode 100644 index 00000000..3be48cdc --- /dev/null +++ b/.agent/skills/push-notification/SKILL.md @@ -0,0 +1,176 @@ +--- +name: "push-notification" +description: "Wavelet 项目专用:当需要开发或接入新的系统通知推送事件、修改消息推送底层设计、调用统一触发器投递消息、或开发带消息推送功能的业务功能时必须使用。本技能指导元数据声明、触发流程、解耦防线和动态同步机制。" +--- + +# 新增消息推送与通知事件开发规范 + +本技能涵盖 Wavelet 的系统通知推送开发规范。开始开发前先阅读仓库根目录 [AGENTS.md](file:///Users/ryan/DEV/Go/Wavelet/AGENTS.md),遵守项目级核心规则。 + +--- + +## 消息推送架构设计 (Architecture) + +Wavelet 的消息推送机制采用了**元数据驱动 + 统一触发器 + 异步任务派发**的解耦设计,其分层及职责划分如下: + +| 目录/包名 | 职责定位 | 包含内容与设计细节 | +| :--- | :--- | :--- | +| **`pkg/push/`** | 推送基础设施层 | 静态定义、不依赖系统数据库和任何框架。定义了统一接口 `Pusher`、单例 `PusherPool` 和多实现(Lark, Webhook, Email 等),提供配置验证及发送功能。 | +| **`internal/apps/admin/push/`** | 通知服务与后台任务层 | 包含以下核心文件:
1. [events.go](file:///Users/ryan/DEV/Go/Wavelet/internal/apps/admin/push/events.go):定义通知事件的结构模型(`NotificationMessage`, `EventMetadata`)、内置事件的动态注册中心(`BuiltInEvents` 及 `RegisterBuiltInEvent` 函数)以及统一触发器类 `EventTrigger`(包括其底层的派发引擎逻辑)。
2. [tasks.go](file:///Users/ryan/DEV/Go/Wavelet/internal/apps/admin/push/tasks.go):定义 Asynq 后台异步发送任务、处理器 `PushHandler` 及其校验逻辑,并记录推送历史审计。
3. [routers.go](file:///Users/ryan/DEV/Go/Wavelet/internal/apps/admin/push/routers.go):管理端接口,负责获取事件配置列表和更新配置。 | +| **`internal/apps/admin/push/custom_events/`** | 自定义通知事件包 | 自定义的事件定义和触发函数单独放到此包下,**一个 Go 文件代表一个事件**。例如:
[admin_login.go](file:///Users/ryan/DEV/Go/Wavelet/internal/apps/admin/push/custom_events/admin_login.go) 代表管理员登录事件。 | +| **数据库审计表** | 状态与历史审计 | `w_push_events` 存放每个通知事件的启用状态、启用渠道、发送目标和自定义渲染模板。
`w_push_histories` 存放消息发送记录用于审计。 | + +--- + +## 核心开发步骤 (Step-by-Step Flow) + +如果某个新业务(如“新用户注册”或“订单创建”)需要带有消息推送功能,请严格按照以下步骤开发: + +### 步骤 1:在 `custom_events/` 中以一个文件声明事件元数据 +在 `internal/apps/admin/push/custom_events/` 下新建一个 Go 文件(如 `user_registered.go`),声明其事件元数据并利用 `init()` 动态注册。 + +```go +package custom_events + +import ( + "context" + "time" + + "github.com/Rain-kl/Wavelet/internal/apps/admin/push" + "github.com/Rain-kl/Wavelet/internal/model" +) + +// NewUserRegistered is the metadata definition for the user registered event. +var NewUserRegistered = push.EventMetadata{ + Key: "user_registered", + Name: "新用户注册提醒", + DefaultTemplate: push.NotificationMessage{ + Title: "新用户注册通知", + Content: "新用户 {{user.username}} (邮箱: {{user.email}}) 于 {{time}} 成功注册。", + Level: "INFO", + }, + Description: "当系统有新用户注册成功时,向管理员或指定目标发送通知", +} + +func init() { + push.RegisterBuiltInEvent(NewUserRegistered) +} +``` + +### 步骤 2:在该事件文件中编写触发封装函数 (Wrapper) +为了使业务层调用方便且类型安全,在同一个事件 Go 文件中为该事件定义一个封装函数。 + +> [!NOTE] +> - 底层框架 `EventTrigger.Trigger` 本身已经内置了**异步 Goroutine 执行**以及 **`context.WithoutCancel(ctx)` 衍生上下文转换**逻辑。 +> - 开发者只需在封装函数中组装数据,并直接调用 `DefaultTrigger.Trigger` 即可,无需在外部手动写 `go func()` 也不需要处理上下文防取消问题,从而通过框架底层强制约束了异步投递行为。 + +```go +// TriggerNewUserRegisteredEvent triggers the user registration notification event. +func TriggerNewUserRegisteredEvent(ctx context.Context, user *model.User) { + if user == nil { + return + } + body := map[string]any{ + "user": user, + "time": time.Now().Format("2006-01-02 15:04:05"), + } + push.DefaultTrigger.Trigger(ctx, NewUserRegistered, body) +} +``` + +### 步骤 3:在业务代码中调用触发函数 +在业务逻辑完成处(例如 `internal/apps/user/routers.go` 的注册 Handler 中)导入 `custom_events` 并调用该函数。 + +```go +import ( + "github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events" +) + +func Register(c *gin.Context) { + // ... 注册成功逻辑 ... + + // 异步触发通知推送事件 + custom_events.TriggerNewUserRegisteredEvent(ctx, user) +} +``` + +### 步骤 4:在主路由器或初始化模块进行匿名导入以确保注册 +由于事件是在 `custom_events` 的 `init()` 中注册到 `push` 包的,所以应用程序的执行路径(例如 [router.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/router.go))必须匿名导入 `custom_events` 包,以确保其在程序启动时被加载和初始化。 + +```go +import ( + _ "github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events" +) +``` + +当程序启动后,系统在初始化阶段的 `SyncEvents` 流程中会自动将新声明的 `user_registered` 事件元数据插入数据库表 `w_push_events` 中。此后,管理员即可直接在管理端前端界面上为该事件配置推送渠道。 + +--- + +## 模板模板渲染与支持的系统变量 (Template Rendering & Variables) + +消息的 `title`、`content` 以及 `ext` 字段中的字符串值都支持变量占位符替换,采用双花括号形式 `{{variable}}`。 + +### 1. 通用事件参数 (Common Variables) +任何通知事件触发时,均支持以下通用参数的渲染: +- `{{time}}`:事件发生/触发的具体时间(格式:`2006-01-02 15:04:05`) + +### 2. 特定事件携带的业务变量 (Event Specific Variables) +特定事件在触发时会携带复杂的业务对象(例如 `user`),可以通过点(`.`)路径语法获取其属性: + +- **管理员登录提醒 (`admin_login`)** + - `{{user.id}}`:管理员 ID + - `{{user.username}}`:管理员用户名 + - `{{user.email}}`:管理员邮箱地址 + - `{{ip}}`:管理员登录来源的客户端 IP + - `{{time}}`:管理员登录成功时间 + +- **新用户注册提醒 (`user_registered`)** + - `{{user.id}}`:新注册用户 ID + - `{{user.username}}`:新注册用户名 + - `{{user.email}}`:新注册用户邮箱地址 + - `{{time}}`:注册成功时间 + +--- + +## 严格遵循事项与防线 (Guardrails) + +### 1. 严格禁止包循环引用 (No Circular Dependencies) +- `custom_events` 包在定义事件时会导入并依赖 `push` 包的 `EventMetadata` 与 `DefaultTrigger` 等底层逻辑。 +- **因此,`push` 包本身绝对不能导入 `custom_events` 包**(否则编译会抛出 package dependency cycle 错误)。 +- 对新事件的自动注册只能在 `custom_events` 内部使用 `init()` 调用 `push.RegisterBuiltInEvent` 来实现。 + +### 2. 单元测试防循环依赖隔离 (Unit Testing Isolation) +- 在对 `push` 包自身进行单元测试(如 `push_test.go`)时,由于 `custom_events` 依赖 `push`,测试文件 `push_test.go` 也无法直接导入 `custom_events`。 +- **防线策略**:在 `push_test.go` 的 `init()` 中本地声明并使用 `RegisterBuiltInEvent` 注册测试专用的 `EventMetadata`,以此来完成 `push` 包的隔离自测。 + +### 3. 禁止绕过统一触发器 (Always Use EventTrigger) +- 所有推送请求必须经过 `EventTrigger.Trigger`,以确保进行“事件是否启用”、“目标渠道过滤”、“全局推送配置读取”及“发送日志审计”等流程。 + +### 4. 代码质量与零 Lint 警报 +- **魔法值防范**:对于推送日志级别(如 `"INFO"`),不要在多个文件里写硬编码字符串,应统一在 `constants.go` 中引用 `defaultLevelInfo` 常量。 +- **命名规范**:不要定义容易造成 Stuttering 的导出类型,例如在 `push` 包内不要使用 `PushSendPayload`,应重命名为 `SendPayload`。 + +--- + +## 验证计划 (Verification Guide) + +1. **授权许可头部补全**: + 新增/修改 Go 文件后,运行自动许可证生成: + ```bash + make license + ``` +2. **Swagger 文档重新构建**: + 如果修改了路由或 Swagger 注释: + ```bash + make swagger + ``` +3. **静态代码质量门禁 (0 Issues)**: + 运行静态检查,必须保证后端与前端均无任何警告: + ```bash + make code-check + ``` +4. **单元测试通过**: + ```bash + go test ./internal/apps/admin/push/... + ``` diff --git a/AGENTS.md b/AGENTS.md index f4a78c41..42745d7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,8 +56,12 @@ - `docs/`:自动生成的 Swagger 文档。请勿手动编辑生成的文件。 - `frontend/`:Next.js 应用。 - `internal/`:私有 Go 后端代码。 +- `pkg/`:公共 Go 库/工具包(留作扩展或存放不依赖特定业务的通用代码)。 - `scripts/`:本地和 CI 辅助脚本。 - `support-files/`:部署和数据库辅助文件。 +- `bin/`:本地编译生成的二进制可执行文件。 +- `data/`:本地运行时数据文件目录(如 PostgreSQL、Redis 数据等)。 +- `uploads/`:本地文件上传存储目录。 后端目录: @@ -78,6 +82,8 @@ - `internal/listener/`:事件监听器和消息/Webhook 消费者。 - `internal/otel_trace/`:链路追踪(tracing)助手。 - `internal/testhelper/`:后端测试共享辅助能力。 +- `internal/buildinfo/`:暴露在发布/构建工作流中注入的元数据(如版本号、编译时间等)。 +- `internal/httppool/`:管理全局共享且经过优化的 HTTP 传输客户端及连接池,并集成 OTel 链路追踪。 前端目录: @@ -99,9 +105,19 @@ - `frontend/components/common/admin/system.tsx`:系统参数管理。 - `frontend/components/common/admin/files.tsx`:上传文件管理。 - `frontend/components/common/admin/users.tsx`:用户管理。 +- `frontend/components/common/admin/access-analytics.tsx`:访问分析与图表展示。 +- `frontend/components/common/admin/access-logs.tsx`:访问日志审计 UI。 +- `frontend/components/common/admin/app-logs.tsx`:应用日志查看 UI。 +- `frontend/components/common/admin/database-manage.tsx`:数据库备份、恢复与管理 UI。 +- `frontend/components/common/admin/file-list.tsx`:管理员文件列表管理组件。 +- `frontend/components/common/admin/file-stats.tsx`:文件存储状态与统计 UI。 +- `frontend/components/common/admin/status.tsx`:系统运行状态与监控 UI。 +- `frontend/components/common/admin/storage-config-tab.tsx`:存储策略与配置 tab 页。 +- `frontend/components/common/admin/system-logs.tsx`:系统日志查看组件。 - `frontend/components/common/general/manage-pannel.tsx`:通用列表/详情管理器。 - `frontend/components/common/general/password-dialog.tsx`:敏感操作密码确认对话框。 - `frontend/components/common/settings/system-settings.tsx`:管理员图形化系统设置。 +- `frontend/components/common/user/file-manager.tsx`:用户端文件管理组件。 ## 开发要求 diff --git a/Makefile b/Makefile index 8205c203..521676eb 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,20 @@ code-check: golangci-lint run cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 +build-backend: + @echo "==> Building backend version=$(VERSION) build_date=$(BUILD_DATE)..." + go build \ + -ldflags "-s -w -X '$(MODULE)/internal/buildinfo.Version=$(VERSION)' -X '$(MODULE)/internal/buildinfo.BuildTime=$(BUILD_DATE)'" \ + -o bin/wavelet \ + main.go + +build-frontend: + @echo "==> Building frontend version=$(VERSION) build_date=$(BUILD_DATE)..." + cd frontend && \ + NEXT_PUBLIC_APP_VERSION="$(VERSION)" \ + NEXT_PUBLIC_APP_BUILD_DATE="$(BUILD_DATE)" \ + pnpm build:embed + build-test: @echo "==> Running frontend and backend build tests in parallel..." @PIDS=""; \ diff --git a/docs/docs.go b/docs/docs.go index d70cdd28..adffef08 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1153,6 +1153,657 @@ const docTemplate = `{ "responses": {} } }, + "/api/v1/admin/push/channels": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统配置的所有消息通道列表,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有消息通道", + "responses": { + "200": { + "description": "消息通道列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PushChannel" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "新建一个消息通道配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "创建消息通道", + "parameters": [ + { + "description": "创建参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.CreateChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PushChannel" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/channels/definitions": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统支持的所有消息通道类型(如飞书、邮件、自定义)的动态表单定义,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有消息通道配置字段定义", + "responses": { + "200": { + "description": "通道配置定义列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/push.Definition" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/channels/test": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "触发一次临时的或现有的通道连通性推送测试,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "测试通道连通性", + "parameters": [ + { + "description": "测试参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.TestChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "测试触发成功", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/admin/push/channels/{id}": { + "put": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "修改消息通道配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "更新消息通道", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "通道ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.UpdateChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PushChannel" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "根据ID删除消息通道,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "删除消息通道", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "通道ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/admin/push/events": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统配置的通知事件列表,包括预置和自定义事件,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有通知事件", + "responses": { + "200": { + "description": "通知事件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PushEvent" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "绑定系统内置事件、推送渠道、接收目标并创建通知事件配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "创建通知事件", + "parameters": [ + { + "description": "创建参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.CreateEventRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PushEvent" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/events/builtin": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统定义的所有内置通知事件元数据,供前端下拉框选择,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有内置通知事件", + "responses": { + "200": { + "description": "内置通知事件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/push.EventMetadata" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/events/{id}": { + "put": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新已有通知事件的推送渠道、接收目标和内容模板,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "更新通知事件", + "parameters": [ + { + "type": "integer", + "description": "事件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.UpdateEventRequest" + } + } + ], + "responses": { + "200": { + "description": "修改成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除数据库中的特定通知事件配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "删除通知事件配置", + "parameters": [ + { + "type": "integer", + "description": "事件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/events/{id}/toggle": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "启用或禁用指定的通知事件", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "快捷切换通知事件启用状态", + "parameters": [ + { + "type": "integer", + "description": "事件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "切换成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/histories": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回分页的通知历史日志数据,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "分页获取通知推送历史", + "parameters": [ + { + "type": "integer", + "description": "当前页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "分页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "过滤事件名称", + "name": "event_key", + "in": "query" + }, + { + "type": "string", + "description": "过滤发送状态", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "推送历史列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/push.pushHistoriesResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/test": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "接收临时通知渠道配置并在本地同步调用 Pusher.Send 发送测试消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "测试推送通道发送", + "parameters": [ + { + "description": "测试请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.TestPushRequest" + } + } + ], + "responses": { + "200": { + "description": "测试成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, "/api/v1/admin/status": { "get": { "security": [ @@ -5284,6 +5935,128 @@ const docTemplate = `{ } } }, + "model.PushChannel": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "description": "备注", + "type": "string" + }, + "enabled": { + "description": "通道是否启用", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "description": "通道名称,仅英文字母和下划线,唯一", + "type": "string" + }, + "other": { + "description": "请求体/SMTP 密码等", + "type": "string" + }, + "token": { + "description": "鉴权令牌或发信用户名等", + "type": "string" + }, + "type": { + "description": "通道类型:custom, lark, email", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "description": "请求地址,HTTPS 协议或 SMTP 地址", + "type": "string" + } + } + }, + "model.PushEvent": { + "type": "object", + "properties": { + "channels": { + "description": "推送渠道列表,如 [\"lark\"]", + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "description": "是否启用", + "type": "boolean" + }, + "event_key": { + "description": "如 admin_login", + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "description": "如 管理员登录", + "type": "string" + }, + "targets": { + "description": "推送目标用户/邮箱列表", + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "description": "消息模板 JSON", + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.PushHistory": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "error_msg": { + "type": "string" + }, + "event_key": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "level": { + "type": "string" + }, + "status": { + "description": "success / failed", + "type": "string" + }, + "target": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "model.Schedule": { "type": "object", "properties": { @@ -5682,6 +6455,282 @@ const docTemplate = `{ } } }, + "push.Config": { + "type": "object", + "properties": { + "channel": { + "description": "渠道名称,例如 \"lark\", \"custom\", \"email\" 等,唯一标识", + "type": "string" + }, + "ext": { + "description": "预留拓展 JSON 配置", + "type": "object", + "additionalProperties": {} + }, + "key": { + "description": "AppID 或 SMTP 用户名", + "type": "string" + }, + "secret": { + "description": "签名密钥或 SMTP 密码/Token", + "type": "string" + }, + "url": { + "description": "Webhook 地址或 SMTP 地址", + "type": "string" + } + } + }, + "push.CreateChannelRequest": { + "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" + } + } + }, + "push.CreateEventRequest": { + "type": "object", + "required": [ + "event_key" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "event_key": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "type": "string" + } + } + }, + "push.Definition": { + "type": "object", + "properties": { + "description": { + "description": "short description", + "type": "string" + }, + "fields": { + "description": "form fields", + "type": "array", + "items": { + "$ref": "#/definitions/push.Field" + } + }, + "name": { + "description": "display name", + "type": "string" + }, + "type": { + "description": "channel type (e.g., custom, lark, email)", + "type": "string" + } + } + }, + "push.EventMetadata": { + "type": "object", + "properties": { + "default_template": { + "$ref": "#/definitions/push.NotificationMessage" + }, + "description": { + "type": "string" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "push.Field": { + "type": "object", + "properties": { + "description": { + "description": "field explanation/help text", + "type": "string" + }, + "key": { + "description": "unique key for the field (e.g. url, token, other)", + "type": "string" + }, + "label": { + "description": "human readable label (e.g. \"Webhook 地址\")", + "type": "string" + }, + "placeholder": { + "description": "input placeholder", + "type": "string" + }, + "required": { + "description": "whether this field is required", + "type": "boolean" + }, + "type": { + "description": "input type: \"text\" | \"password\" | \"textarea\"", + "type": "string" + } + } + }, + "push.NotificationMessage": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "ext": { + "type": "object", + "additionalProperties": {} + }, + "level": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "push.TestChannelRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "target": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "push.TestPushRequest": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/push.Config" + }, + "target": { + "type": "string" + } + } + }, + "push.UpdateChannelRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "push.UpdateEventRequest": { + "type": "object", + "required": [ + "template" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "type": "string" + } + } + }, + "push.pushHistoriesResponse": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PushHistory" + } + }, + "total": { + "type": "integer" + } + } + }, "status.DatabaseInfoResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index cddb8371..9ae3e408 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1146,6 +1146,657 @@ "responses": {} } }, + "/api/v1/admin/push/channels": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统配置的所有消息通道列表,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有消息通道", + "responses": { + "200": { + "description": "消息通道列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PushChannel" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "新建一个消息通道配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "创建消息通道", + "parameters": [ + { + "description": "创建参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.CreateChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PushChannel" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/channels/definitions": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统支持的所有消息通道类型(如飞书、邮件、自定义)的动态表单定义,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有消息通道配置字段定义", + "responses": { + "200": { + "description": "通道配置定义列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/push.Definition" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/channels/test": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "触发一次临时的或现有的通道连通性推送测试,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "测试通道连通性", + "parameters": [ + { + "description": "测试参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.TestChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "测试触发成功", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/admin/push/channels/{id}": { + "put": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "修改消息通道配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "更新消息通道", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "通道ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.UpdateChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PushChannel" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "根据ID删除消息通道,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "删除消息通道", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "通道ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/admin/push/events": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统配置的通知事件列表,包括预置和自定义事件,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有通知事件", + "responses": { + "200": { + "description": "通知事件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PushEvent" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "绑定系统内置事件、推送渠道、接收目标并创建通知事件配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "创建通知事件", + "parameters": [ + { + "description": "创建参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.CreateEventRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PushEvent" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/events/builtin": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统定义的所有内置通知事件元数据,供前端下拉框选择,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "获取所有内置通知事件", + "responses": { + "200": { + "description": "内置通知事件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/push.EventMetadata" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/events/{id}": { + "put": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新已有通知事件的推送渠道、接收目标和内容模板,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "更新通知事件", + "parameters": [ + { + "type": "integer", + "description": "事件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.UpdateEventRequest" + } + } + ], + "responses": { + "200": { + "description": "修改成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除数据库中的特定通知事件配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "删除通知事件配置", + "parameters": [ + { + "type": "integer", + "description": "事件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/events/{id}/toggle": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "启用或禁用指定的通知事件", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "快捷切换通知事件启用状态", + "parameters": [ + { + "type": "integer", + "description": "事件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "切换成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/histories": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回分页的通知历史日志数据,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "分页获取通知推送历史", + "parameters": [ + { + "type": "integer", + "description": "当前页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "分页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "过滤事件名称", + "name": "event_key", + "in": "query" + }, + { + "type": "string", + "description": "过滤发送状态", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "推送历史列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/push.pushHistoriesResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/push/test": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "接收临时通知渠道配置并在本地同步调用 Pusher.Send 发送测试消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-push" + ], + "summary": "测试推送通道发送", + "parameters": [ + { + "description": "测试请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/push.TestPushRequest" + } + } + ], + "responses": { + "200": { + "description": "测试成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, "/api/v1/admin/status": { "get": { "security": [ @@ -5277,6 +5928,128 @@ } } }, + "model.PushChannel": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "description": "备注", + "type": "string" + }, + "enabled": { + "description": "通道是否启用", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "description": "通道名称,仅英文字母和下划线,唯一", + "type": "string" + }, + "other": { + "description": "请求体/SMTP 密码等", + "type": "string" + }, + "token": { + "description": "鉴权令牌或发信用户名等", + "type": "string" + }, + "type": { + "description": "通道类型:custom, lark, email", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "description": "请求地址,HTTPS 协议或 SMTP 地址", + "type": "string" + } + } + }, + "model.PushEvent": { + "type": "object", + "properties": { + "channels": { + "description": "推送渠道列表,如 [\"lark\"]", + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "description": "是否启用", + "type": "boolean" + }, + "event_key": { + "description": "如 admin_login", + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "description": "如 管理员登录", + "type": "string" + }, + "targets": { + "description": "推送目标用户/邮箱列表", + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "description": "消息模板 JSON", + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.PushHistory": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "error_msg": { + "type": "string" + }, + "event_key": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "level": { + "type": "string" + }, + "status": { + "description": "success / failed", + "type": "string" + }, + "target": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "model.Schedule": { "type": "object", "properties": { @@ -5675,6 +6448,282 @@ } } }, + "push.Config": { + "type": "object", + "properties": { + "channel": { + "description": "渠道名称,例如 \"lark\", \"custom\", \"email\" 等,唯一标识", + "type": "string" + }, + "ext": { + "description": "预留拓展 JSON 配置", + "type": "object", + "additionalProperties": {} + }, + "key": { + "description": "AppID 或 SMTP 用户名", + "type": "string" + }, + "secret": { + "description": "签名密钥或 SMTP 密码/Token", + "type": "string" + }, + "url": { + "description": "Webhook 地址或 SMTP 地址", + "type": "string" + } + } + }, + "push.CreateChannelRequest": { + "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" + } + } + }, + "push.CreateEventRequest": { + "type": "object", + "required": [ + "event_key" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "event_key": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "type": "string" + } + } + }, + "push.Definition": { + "type": "object", + "properties": { + "description": { + "description": "short description", + "type": "string" + }, + "fields": { + "description": "form fields", + "type": "array", + "items": { + "$ref": "#/definitions/push.Field" + } + }, + "name": { + "description": "display name", + "type": "string" + }, + "type": { + "description": "channel type (e.g., custom, lark, email)", + "type": "string" + } + } + }, + "push.EventMetadata": { + "type": "object", + "properties": { + "default_template": { + "$ref": "#/definitions/push.NotificationMessage" + }, + "description": { + "type": "string" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "push.Field": { + "type": "object", + "properties": { + "description": { + "description": "field explanation/help text", + "type": "string" + }, + "key": { + "description": "unique key for the field (e.g. url, token, other)", + "type": "string" + }, + "label": { + "description": "human readable label (e.g. \"Webhook 地址\")", + "type": "string" + }, + "placeholder": { + "description": "input placeholder", + "type": "string" + }, + "required": { + "description": "whether this field is required", + "type": "boolean" + }, + "type": { + "description": "input type: \"text\" | \"password\" | \"textarea\"", + "type": "string" + } + } + }, + "push.NotificationMessage": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "ext": { + "type": "object", + "additionalProperties": {} + }, + "level": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "push.TestChannelRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "other": { + "type": "string" + }, + "target": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "push.TestPushRequest": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/push.Config" + }, + "target": { + "type": "string" + } + } + }, + "push.UpdateChannelRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "other": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "push.UpdateEventRequest": { + "type": "object", + "required": [ + "template" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "template": { + "type": "string" + } + } + }, + "push.pushHistoriesResponse": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/model.PushHistory" + } + }, + "total": { + "type": "integer" + } + } + }, "status.DatabaseInfoResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 08492a0b..511f3b95 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -306,6 +306,91 @@ definitions: id: type: integer type: object + model.PushChannel: + properties: + created_at: + type: string + description: + description: 备注 + type: string + enabled: + description: 通道是否启用 + type: boolean + id: + type: integer + name: + description: 通道名称,仅英文字母和下划线,唯一 + type: string + other: + description: 请求体/SMTP 密码等 + type: string + token: + description: 鉴权令牌或发信用户名等 + type: string + type: + description: 通道类型:custom, lark, email + type: string + updated_at: + type: string + url: + description: 请求地址,HTTPS 协议或 SMTP 地址 + type: string + type: object + model.PushEvent: + properties: + channels: + description: 推送渠道列表,如 ["lark"] + items: + type: string + type: array + created_at: + type: string + enabled: + description: 是否启用 + type: boolean + event_key: + description: 如 admin_login + type: string + id: + type: integer + name: + description: 如 管理员登录 + type: string + targets: + description: 推送目标用户/邮箱列表 + items: + type: string + type: array + template: + description: 消息模板 JSON + type: string + updated_at: + type: string + type: object + model.PushHistory: + properties: + channel: + type: string + content: + type: string + created_at: + type: string + error_msg: + type: string + event_key: + type: string + id: + type: integer + level: + type: string + status: + description: success / failed + type: string + target: + type: string + title: + type: string + type: object model.Schedule: properties: created_at: @@ -577,6 +662,192 @@ definitions: user: $ref: '#/definitions/oauth.BasicUserInfo' type: object + push.Config: + properties: + channel: + description: 渠道名称,例如 "lark", "custom", "email" 等,唯一标识 + type: string + ext: + additionalProperties: {} + description: 预留拓展 JSON 配置 + type: object + key: + description: AppID 或 SMTP 用户名 + type: string + secret: + description: 签名密钥或 SMTP 密码/Token + type: string + url: + description: Webhook 地址或 SMTP 地址 + type: string + type: object + push.CreateChannelRequest: + 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 + push.CreateEventRequest: + properties: + channels: + items: + type: string + type: array + enabled: + type: boolean + event_key: + type: string + targets: + items: + type: string + type: array + template: + type: string + required: + - event_key + type: object + push.Definition: + properties: + description: + description: short description + type: string + fields: + description: form fields + items: + $ref: '#/definitions/push.Field' + type: array + name: + description: display name + type: string + type: + description: channel type (e.g., custom, lark, email) + type: string + type: object + push.EventMetadata: + properties: + default_template: + $ref: '#/definitions/push.NotificationMessage' + description: + type: string + key: + type: string + name: + type: string + type: object + push.Field: + properties: + description: + description: field explanation/help text + type: string + key: + description: unique key for the field (e.g. url, token, other) + type: string + label: + description: human readable label (e.g. "Webhook 地址") + type: string + placeholder: + description: input placeholder + type: string + required: + description: whether this field is required + type: boolean + type: + description: 'input type: "text" | "password" | "textarea"' + type: string + type: object + push.NotificationMessage: + properties: + content: + type: string + ext: + additionalProperties: {} + type: object + level: + type: string + title: + type: string + type: object + push.TestChannelRequest: + properties: + name: + type: string + other: + type: string + target: + type: string + token: + type: string + type: + type: string + url: + type: string + type: object + push.TestPushRequest: + properties: + config: + $ref: '#/definitions/push.Config' + target: + type: string + required: + - config + type: object + push.UpdateChannelRequest: + properties: + description: + type: string + enabled: + type: boolean + other: + type: string + token: + type: string + type: + type: string + url: + type: string + required: + - type + type: object + push.UpdateEventRequest: + properties: + channels: + items: + type: string + type: array + enabled: + type: boolean + targets: + items: + type: string + type: array + template: + type: string + required: + - template + type: object + push.pushHistoriesResponse: + properties: + results: + items: + $ref: '#/definitions/model.PushHistory' + type: array + total: + type: integer + type: object status.DatabaseInfoResponse: properties: name: @@ -1820,6 +2091,381 @@ paths: summary: 系统日志实时推送 tags: - admin + /api/v1/admin/push/channels: + get: + description: 返回系统配置的所有消息通道列表,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 消息通道列表 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + items: + $ref: '#/definitions/model.PushChannel' + type: array + type: object + security: + - SessionCookie: [] + summary: 获取所有消息通道 + tags: + - admin-push + post: + consumes: + - application/json + description: 新建一个消息通道配置,需要管理员权限 + parameters: + - description: 创建参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/push.CreateChannelRequest' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + $ref: '#/definitions/model.PushChannel' + type: object + security: + - SessionCookie: [] + summary: 创建消息通道 + tags: + - admin-push + /api/v1/admin/push/channels/{id}: + delete: + description: 根据ID删除消息通道,需要管理员权限 + parameters: + - description: 通道ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/util.ResponseAny' + security: + - SessionCookie: [] + summary: 删除消息通道 + tags: + - admin-push + put: + consumes: + - application/json + description: 修改消息通道配置,需要管理员权限 + parameters: + - description: 通道ID + format: int64 + in: path + name: id + required: true + type: integer + - description: 更新参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/push.UpdateChannelRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + $ref: '#/definitions/model.PushChannel' + type: object + security: + - SessionCookie: [] + summary: 更新消息通道 + tags: + - admin-push + /api/v1/admin/push/channels/definitions: + get: + description: 返回系统支持的所有消息通道类型(如飞书、邮件、自定义)的动态表单定义,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 通道配置定义列表 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + items: + $ref: '#/definitions/push.Definition' + type: array + type: object + security: + - SessionCookie: [] + summary: 获取所有消息通道配置字段定义 + tags: + - admin-push + /api/v1/admin/push/channels/test: + post: + consumes: + - application/json + description: 触发一次临时的或现有的通道连通性推送测试,需要管理员权限 + parameters: + - description: 测试参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/push.TestChannelRequest' + produces: + - application/json + responses: + "200": + description: 测试触发成功 + schema: + $ref: '#/definitions/util.ResponseAny' + security: + - SessionCookie: [] + summary: 测试通道连通性 + tags: + - admin-push + /api/v1/admin/push/events: + get: + description: 返回系统配置的通知事件列表,包括预置和自定义事件,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 通知事件列表 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + items: + $ref: '#/definitions/model.PushEvent' + type: array + type: object + security: + - SessionCookie: [] + summary: 获取所有通知事件 + tags: + - admin-push + post: + consumes: + - application/json + description: 绑定系统内置事件、推送渠道、接收目标并创建通知事件配置,需要管理员权限 + parameters: + - description: 创建参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/push.CreateEventRequest' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + $ref: '#/definitions/model.PushEvent' + type: object + security: + - SessionCookie: [] + summary: 创建通知事件 + tags: + - admin-push + /api/v1/admin/push/events/{id}: + delete: + description: 删除数据库中的特定通知事件配置,需要管理员权限 + parameters: + - description: 事件 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + type: string + type: object + security: + - SessionCookie: [] + summary: 删除通知事件配置 + tags: + - admin-push + put: + consumes: + - application/json + description: 更新已有通知事件的推送渠道、接收目标和内容模板,需要管理员权限 + parameters: + - description: 事件 ID + in: path + name: id + required: true + type: integer + - description: 更新参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/push.UpdateEventRequest' + produces: + - application/json + responses: + "200": + description: 修改成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + type: string + type: object + security: + - SessionCookie: [] + summary: 更新通知事件 + tags: + - admin-push + /api/v1/admin/push/events/{id}/toggle: + post: + description: 启用或禁用指定的通知事件 + parameters: + - description: 事件 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 切换成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + type: string + type: object + security: + - SessionCookie: [] + summary: 快捷切换通知事件启用状态 + tags: + - admin-push + /api/v1/admin/push/events/builtin: + get: + description: 返回系统定义的所有内置通知事件元数据,供前端下拉框选择,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 内置通知事件列表 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + items: + $ref: '#/definitions/push.EventMetadata' + type: array + type: object + security: + - SessionCookie: [] + summary: 获取所有内置通知事件 + tags: + - admin-push + /api/v1/admin/push/histories: + get: + description: 返回分页的通知历史日志数据,需要管理员权限 + parameters: + - description: 当前页码 + in: query + name: page + type: integer + - description: 分页大小 + in: query + name: page_size + type: integer + - description: 过滤事件名称 + in: query + name: event_key + type: string + - description: 过滤发送状态 + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: 推送历史列表 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + $ref: '#/definitions/push.pushHistoriesResponse' + type: object + security: + - SessionCookie: [] + summary: 分页获取通知推送历史 + tags: + - admin-push + /api/v1/admin/push/test: + post: + consumes: + - application/json + description: 接收临时通知渠道配置并在本地同步调用 Pusher.Send 发送测试消息 + parameters: + - description: 测试请求体 + in: body + name: request + required: true + schema: + $ref: '#/definitions/push.TestPushRequest' + produces: + - application/json + responses: + "200": + description: 测试成功 + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + type: string + type: object + security: + - SessionCookie: [] + summary: 测试推送通道发送 + tags: + - admin-push /api/v1/admin/status: get: description: 获取后端服务运行状态、Goroutine、内存指标等详细统计数据,需要管理员权限 diff --git a/frontend/app/(main)/admin/push/page.tsx b/frontend/app/(main)/admin/push/page.tsx new file mode 100644 index 00000000..d8f2eea5 --- /dev/null +++ b/frontend/app/(main)/admin/push/page.tsx @@ -0,0 +1,14 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +import type {Metadata} from "next"; +import {PushNotificationManager} from "@/components/common/admin/push-notification"; + +export const metadata: Metadata = { + title: "通知推送 - Wavelet Admin", + description: "系统通知多渠道推送与事件管理控制台", +}; + +export default function PushAdminPage() { + return ; +} diff --git a/frontend/components/common/admin/push-notification.tsx b/frontend/components/common/admin/push-notification.tsx new file mode 100644 index 00000000..e72a5eea --- /dev/null +++ b/frontend/components/common/admin/push-notification.tsx @@ -0,0 +1,65 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import * as React from "react" +import {motion} from "motion/react" +import {Bell, History, Layers, Settings} from "lucide-react" + +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" +import {EventsTab} from "./push/events-tab" +import {HistoriesTab} from "./push/histories-tab" +import {SettingsTab} from "./push/settings-tab" + +export function PushNotificationManager() { + const [activeTab, setActiveTab] = React.useState("events") + + return ( + +
+ +
+

通知推送管理

+
+
+ + + + + + 事件管理 + + + + 通知历史 + + + + 通道管理与设置 + + + + {/* ==================== 1. 事件管理 TAB ==================== */} + + + + + {/* ==================== 2. 通知历史 TAB ==================== */} + + + + + {/* ==================== 3. 通道管理与设置 TAB ==================== */} + + + + +
+ ) +} diff --git a/frontend/components/common/admin/push/events-tab.tsx b/frontend/components/common/admin/push/events-tab.tsx new file mode 100644 index 00000000..269b764b --- /dev/null +++ b/frontend/components/common/admin/push/events-tab.tsx @@ -0,0 +1,609 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import * as React from "react" +import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" +import {toast} from "sonner" +import {ChevronDown, Edit2, Layers, Loader2, Plus, Trash2,} from "lucide-react" + +import {Button} from "@/components/ui/button" +import {Input} from "@/components/ui/input" +import {Label} from "@/components/ui/label" +import {Textarea} from "@/components/ui/textarea" +import {Switch} from "@/components/ui/switch" +import {Badge} from "@/components/ui/badge" +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" +import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select" +import {Checkbox} from "@/components/ui/checkbox" +import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover" + +import {ErrorInline} from "@/components/layout/error" +import {LoadingStateWithBorder} from "@/components/layout/loading" +import {EmptyStateWithBorder} from "@/components/layout/empty" + + +import services from "@/lib/services" +import type {CreatePushEventRequest, PushEvent, UpdatePushEventRequest,} from "@/lib/services/push" + +export function EventsTab() { + const queryClient = useQueryClient() + + // --- 获取所有自定义消息通道 --- + const channelsQuery = useQuery({ + queryKey: ["admin", "push-channels"], + queryFn: () => services.push.listChannels(), + }) + + const availableChannels = React.useMemo(() => { + const customChannels = (channelsQuery.data ?? []) + .filter(c => c.enabled) + .map(c => c.name) + return ["email", ...customChannels] + }, [channelsQuery.data]) + + // --- 获取通知事件 --- + const eventsQuery = useQuery({ + queryKey: ["admin", "push-events"], + queryFn: () => services.push.listEvents(), + }) + + const builtInEventsQuery = useQuery({ + queryKey: ["admin", "push-builtin-events"], + queryFn: () => services.push.listBuiltInEvents(), + }) + + // --- 修改保存事件 Mutation --- + const updateEventMutation = useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => services.push.updateEvent(id, data), + onSuccess: () => { + toast.success("事件更新成功") + queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) + setEditEventOpen(false) + }, + onError: (err: unknown) => { + toast.error("事件更新失败: " + (err as Error).message) + }, + }) + + const toggleEventMutation = useMutation({ + mutationFn: (id: number) => services.push.toggleEvent(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) + }, + onError: (err: unknown) => { + toast.error("操作失败: " + (err as Error).message) + }, + }) + + const createEventMutation = useMutation({ + mutationFn: (data: CreatePushEventRequest) => services.push.createEvent(data), + onSuccess: () => { + toast.success("事件创建成功") + queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) + setCreateEventOpen(false) + setNewEventKey("") + setNewEventChannels([]) + setNewEventEnabled(true) + }, + onError: (err: unknown) => { + toast.error("事件创建失败: " + (err as Error).message) + }, + }) + + const deleteEventMutation = useMutation({ + mutationFn: (id: number) => services.push.deleteEvent(id), + onSuccess: () => { + toast.success("配置删除成功") + queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) + }, + onError: (err: unknown) => { + toast.error("配置删除失败: " + (err as Error).message) + }, + }) + + // --- 事件编辑对话框状态 --- + const [editEventOpen, setEditEventOpen] = React.useState(false) + const [selectedEvent, setSelectedEvent] = React.useState(null) + const [eventChannels, setEventChannels] = React.useState([]) + const [eventTargets, setEventTargets] = React.useState("") + const [eventTemplate, setEventTemplate] = React.useState("") + + // 事件创建对话框状态 + const [createEventOpen, setCreateEventOpen] = React.useState(false) + const [newEventKey, setNewEventKey] = React.useState("") + const [newEventChannels, setNewEventChannels] = React.useState([]) + const [newEventTargets, setNewEventTargets] = React.useState("") + const [newEventTemplate, setNewEventTemplate] = React.useState("") + const [newEventEnabled, setNewEventEnabled] = React.useState(true) + + const availableBuiltInEvents = React.useMemo(() => { + const configuredKeys = new Set((eventsQuery.data ?? []).map(e => e.event_key)) + return (builtInEventsQuery.data ?? []).filter(e => !configuredKeys.has(e.key)) + }, [builtInEventsQuery.data, eventsQuery.data]) + + const handleEditEventClick = (event: PushEvent) => { + setSelectedEvent(event) + setEventChannels(event.channels) + setEventTargets((event.targets ?? []).join(", ")) + setEventTemplate(event.template) + setEditEventOpen(true) + } + + const handleSaveEvent = () => { + if (!selectedEvent) return + + try { + JSON.parse(eventTemplate) + } catch { + toast.error("消息模板不是合法的 JSON 格式") + return + } + + const targets = eventTargets + .split(",") + .map(t => t.trim()) + .filter(t => t !== "") + + updateEventMutation.mutate({ + id: selectedEvent.id, + data: { + channels: eventChannels, + targets, + template: eventTemplate, + enabled: selectedEvent.enabled, + }, + }) + } + + const handleCreateEventClick = () => { + setNewEventKey("") + setNewEventChannels([]) + setNewEventTargets("") + setNewEventTemplate("") + setNewEventEnabled(true) + setCreateEventOpen(true) + } + + const handleNewEventKeyChange = (key: string) => { + setNewEventKey(key) + const ev = availableBuiltInEvents.find(e => e.key === key) + if (ev) { + setNewEventTemplate(JSON.stringify(ev.default_template, null, 2)) + } else { + setNewEventTemplate("") + } + } + + const handleCreateEvent = () => { + if (!newEventKey) { + toast.error("请选择系统事件") + return + } + + if (newEventTemplate) { + try { + JSON.parse(newEventTemplate) + } catch { + toast.error("内容渲染模板不是合法的 JSON 格式") + return + } + } + + const targets = newEventTargets + .split(",") + .map(t => t.trim()) + .filter(t => t !== "") + + createEventMutation.mutate({ + event_key: newEventKey, + channels: newEventChannels, + targets: targets.length > 0 ? targets : undefined, + template: newEventTemplate || undefined, + enabled: newEventEnabled, + }) + } + + return ( +
+
+ +
+ {eventsQuery.isLoading ? ( + + ) : eventsQuery.isError ? ( +
+ eventsQuery.refetch()} className="justify-center" /> +
+ ) : (eventsQuery.data ?? []).length === 0 ? ( + + ) : ( +
+ + + + ID + 通知事件 + 关联渠道 + 推送目标 + 状态 + 操作 + + + + {(eventsQuery.data ?? []).map((event) => ( + handleEditEventClick(event)} + > + {event.id} + +
+ {event.name} + {event.event_key} +
+
+ +
+ {(event.channels ?? []).length === 0 ? ( + 未指定渠道 + ) : ( + event.channels.map(ch => ( + + {ch === "email" ? "邮件" : ch} + + )) + )} +
+
+ +
+ {(!event.targets || event.targets.length === 0) ? ( + - + ) : ( + event.targets.map(t => ( + + {t} + + )) + )} +
+
+ e.stopPropagation()}> + toggleEventMutation.mutate(event.id)} + className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7" + /> + + e.stopPropagation()}> +
+ + + + + + + 配置 + + + + + + + + + + + 删除 + + + +
+
+
+ ))} +
+
+
+ )} + + {/* ==================== 对话框:新增事件 ==================== */} + + + + 新增通知事件 + + 选择系统内置的事件并绑定推送渠道,创建后即可开始生效 + + + +
+
+ + {builtInEventsQuery.isLoading ? ( +
+ + 加载系统事件中... +
+ ) : availableBuiltInEvents.length === 0 ? ( +
+ 所有内置事件都已配置,没有可新增的事件。 +
+ ) : ( + + )} +
+ + {newEventKey && ( +
+ 事件说明: + + {availableBuiltInEvents.find(e => e.key === newEventKey)?.description || "无描述信息"} + +
+ )} + +
+ + + + + + +
+ {availableChannels.map(ch => ( + + ))} + {availableChannels.length === 0 && ( +
+ 暂无可用渠道,请先在“通道管理与设置”中创建或启用。 +
+ )} +
+
+
+
+ +
+ + setNewEventTargets(e.target.value)} + className="text-xs h-9" + /> +
+ +
+
+ + + 支持变量:{"{{user.username}}"}, {"{{ip}}"}, {"{{time}}"} + +
+