# NotifyCenter **Repository Path**: wosperry/notify-center ## Basic Information - **Project Name**: NotifyCenter - **Description**: 一个通知中转项目,慢慢写。以后再改介绍 - **Primary Language**: C# - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-11-07 - **Last Updated**: 2025-12-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # NotifyCenter 基于 .NET 8 的统一通知综合管理平台,提供以下能力: 1. 统一管理项目与通知规则,可为每个规则配置不同的 Sender 以及 Webhook。 2. 通过 `SenderController` 暴露标准接入接口,将业务消息映射到不同 Sender(当前支持飞书、Mock Sender 等)。 3. 使用持久化队列 + 后台任务进行发送调度,确保流程不依赖外部接口,并持久化每条消息状态。 4. 支持发送完成 Webhook 回调,让业务系统实时获知成功/失败并进行后续处理。 --- ## 目录结构说明 | 项目 | 说明 | | --- | --- | | `web/NotifyCenter.Web` | ASP.NET Core Web API,提供项目/通知接口、管理界面以及 EF Core 迁移。 | | `core/NotifyCenter.Application` | 应用服务层,包含队列调度以及通知/Webhook 分发逻辑。 | | `core/NotifyCenter.Repository` | `NotifyCenterDbContext` 以及实体配置。 | | `core/NotifyCenter.Models` | 实体模型以及枚举定义。 | | `sender/NotifyCenter.Sender` | Sender 接口 `ISender` 定义。 | | `sender/NotifyCenter.Sender.Feishu` | 飞书 Sender 实现,群卡片、普通消息等。 | | `sender/NotifyCenter.Sender.Mock` | 测试用文本 Sender,用于演示/调试。 | --- ## 核心功能 1. **项目 / 通知规则管理** - `ProjectController` (`/project`) 提供项目 CRUD 与通知规则管理接口。 - 通过 `NotificationRuleRequest` 配置规则名称、描述、类型(Sender)、业务配置 JSON、Webhook 及重试/定时策略等。 2. **消息发送(统一入队)** - 业务系统通过 `POST /api/sender/publish` 提交消息:`projectId / notificationId / receiver / content`。 - 其中 `receiver` 和 `content` 建议都使用 JSON 对象或数组;`content` 在队列中统一存为结构化 JSON,由各 Sender(如 `FeishuGroupCommonMessage`)内部解析并拼装最终文本(例如 `[title] content` 形式)。 - 服务端会根据项目/规则写入 `NotificationRecord`(记录当次快照)并追加一条 `QueueMessage`,由后台任务异步发送。 3. **队列调度与重试** - `QueueMessageDispatcher` 按 `AvailableAt` 顺序拉取消息,并匹配对应 `ISender` 实例发送,默认 HTTP 超时时间 30s。 - 发送失败会记录错误、递增 `Attempt`,并根据规则 `MaxRetryTimes` 决定重试或标记为失败。 4. **Webhook 回调** - `NotificationStatusService` 在消息状态变化时负责构造 `NotificationWebhookPayload`。 - `NotificationWebhookDispatcher` 使用 `IHttpClientFactory` 异步回调目标系统,并在启用密钥时附带 `X-NotifyCenter-Signature`(HMAC-SHA256)。 --- ## 通知规则配置字段 | 字段 | 说明 | | --- | --- | | Type | Sender 类型字符串,需与 `ISender.Type` 保持一致,可通过 `GET /project/sender-types` 获取可选值。 | | ConfigurationJson | Sender 专用配置,JSON 字符串格式,需符合对应 `ISender` 的要求。 | | `EnableWebhook` | 是否启用 webhook,启用时 `WebhookUrl`、`WebhookSecret` 会被校验。 | | `WebhookUrl` | Webhook 地址,启用 webhook 时,必须为有效的 URL。 | | `WebhookSecret` | 可选签名密钥,最多 255 字符,留空表示不校验。 | | `MaxRetryTimes` | 发送失败的重试次数,范围 1~10,默认 3。 | | `ScheduleMode` | 调度模式:`Immediate`(立即发送)、`Delay`(延迟 N 秒)、`Scheduled`(指定时间点)。 | | `ScheduleDelaySeconds` | 延迟秒数(`ScheduleMode = Delay` 时必填)。 | | `ScheduleTimeOfDay` | 指定发送时间点(`ScheduleMode = Scheduled` 时必填,格式:`HH:mm:ss`)。 | | `ScheduleTimeZone` | 时区(`ScheduleMode = Scheduled` 时必填,例如:`Asia/Shanghai`)。 | > 所有 DTO 均使用 DataAnnotations + 应用层校验(URL、字段范围等),防止外部直接写实体字段。 --- ## 重试机制与超时 - **重试策略** - 默认 3 次,可通过通知规则配置范围 1~10。 - 失败后由 `QueueMessageDispatcher` 更新 `QueueMessage.AvailableAt` 延迟重试,可按需实现更复杂的退避策略。 - 同一 `NotificationRecord` 会记录 `RetryTime`(递增计数),`QueueMessage` 也会记录每次状态变更。 - **Webhook & Sender 超时** - 所有 Sender 使用 `HttpClient`,默认超时 30 秒以避免阻塞线程。 - Webhook Handler 使用 `IHttpClientFactory` 默认超时配置,可在 `Program.cs` 自定义客户端配置。 - 超时只会记录日志,不会中断流程,如需补偿可扩展新的 Handler。 --- ## Webhook 规范 ### 请求约定 | 项 | 说明 | | --- | --- | | Method | `POST` | | Content-Type | `application/json; charset=utf-8` | | Header | `X-NotifyCenter-Signature`(当配置 `WebhookSecret` 时提供),值为 `HMACSHA256(secret, body)` 的十六进制小写字符串。 | | Body | 见下方示例。 | | 响应 | `200 OK` 视为成功,其他状态码记录为警告(当前不重试,可扩展)。 | ```jsonc { "recordId": 9123456, "projectId": 1001, "notificationRuleId": 501, "notificationName": "系统告警", "notificationType": "FeishuGroupCard", "notificationTypeValue": 8, "status": "Success", "statusValue": 3, "statusMessage": "发送成功", "occurredAtUtc": "2025-11-09T12:35:48.123Z" } ``` ### C# (.NET / ASP.NET Core) ```csharp private static bool VerifySignature(string payload, string? signature, string secret) { if (string.IsNullOrWhiteSpace(signature)) { return false; } using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload))).ToLowerInvariant(); return CryptographicOperations.FixedTimeEquals( Convert.FromHexString(signature), Convert.FromHexString(expected)); } app.MapPost("/notify", async (HttpRequest request, ILoggerFactory loggerFactory) => { var payload = await new StreamReader(request.Body).ReadToEndAsync(); var signature = request.Headers["X-NotifyCenter-Signature"].FirstOrDefault(); if (!VerifySignature(payload, signature, configuration["WebhookSecret"])) { return Results.Unauthorized(); } var data = JsonSerializer.Deserialize(payload); // TODO: handle data return Results.Ok(); }); ``` ### Node.js (Express) ```javascript function verifySignature(payload, signature, secret) { if (!signature) return false; const hmac = crypto.createHmac('sha256', secret); const expected = hmac.update(payload, 'utf8').digest('hex'); return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex')); } app.post('/notify', express.text({ type: '*/*' }), (req, res) => { const payload = req.body; const signature = req.get('X-NotifyCenter-Signature'); if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('invalid signature'); } const data = JSON.parse(payload); // TODO: handle data res.sendStatus(200); }); ``` ### Python (FastAPI) ```python def verify_signature(payload: bytes, signature: str | None, secret: str) -> bool: if not signature: return False expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(signature, expected) @app.post("/notify") async def notify(request: Request): payload = await request.body() signature = request.headers.get("X-NotifyCenter-Signature") if not verify_signature(payload, signature, settings.webhook_secret): raise HTTPException(status_code=401, detail="invalid signature") data = json.loads(payload) # TODO: handle data return {"ok": True} ``` ### Java (Spring Boot) ```java public boolean verifySignature(byte[] body, String signature, String secret) throws Exception { if (signature == null || signature.isBlank()) { return false; } Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), mac.getAlgorithm())); var expected = HexFormat.of().formatHex(mac.doFinal(body)); return MessageDigest.isEqual( HexFormat.of().parseHex(signature), HexFormat.of().parseHex(expected)); } @PostMapping(path = "/notify", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity notify(@RequestBody byte[] body, @RequestHeader("X-NotifyCenter-Signature") String signature) throws Exception { if (!verifySignature(body, signature, webhookSecret)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } var payload = objectMapper.readValue(body, WebhookPayload.class); // TODO: handle payload return ResponseEntity.ok().build(); } ``` --- ## 快速开始 & 迁移 1. 安装 .NET 8 SDK、PostgreSQL,并在 `web/NotifyCenter.Web/appsettings.Development.json` 配置 `ConnectionStrings:NotifyCenter`。 2. 运行 `dotnet tool restore`(确保 `dotnet-ef` 等工具可用)。 3. 执行迁移:`cd web/NotifyCenter.Web && dotnet tool run dotnet-ef database update --context NotifyCenterMigrateDbContext`。 4. 启动 Web 服务:`dotnet run --project web/NotifyCenter.Web/NotifyCenter.Web.csproj`,通过 `https://localhost:5001/swagger` 验证接口。 5. 使用 `POST /api/sender/publish` 携带 `projectId / notificationId / receiver / content` 测试消息入队(需 `X-NotifyCenter-Token`)。 --- ## 开发者部署指南 1. **准备环境**:Fork/Clone 仓库,安装 .NET 8 SDK 和 PostgreSQL,确保连接字符串正确。 2. **迁移数据库**:执行 `dotnet tool restore` 和 `dotnet tool run dotnet-ef database update --context NotifyCenterMigrateDbContext`。 3. **启动服务**:`dotnet run --project web/NotifyCenter.Web/NotifyCenter.Web.csproj`,需要测试 webhook 时可同时启动 `web/NotifyCenter.WebTest`。 4. **配置通知规则**:通过接口或数据库直接创建,`Type` 可选 `FeishuGroupCard`、`FeishuGroupCommonMessage`、`FeishuCommonMessage`、`MockFileMessage`。Mock Sender 可使用 `{"output_path": "MockSender/messages.txt"}` 配置。 5. **验证发送链路**:通过 `POST /api/sender/publish` 提交测试消息,观察日志或 `MockSender/messages.txt` 以确认链路正确。 6. **扩展 Sender**:实现 `ISender` + `NotificationType`,参考 README 说明,提交前运行 `dotnet build NotifyCenter.sln`。 ## 管理后台使用指南 1. 访问 `web/NotifyCenter.Web` 并打开 `https://localhost:5001/admin`,使用管理员密码登录后管理项目。 2. 在项目详情页添加通知规则,选择 `Type`(Sender 名称),并将对应配置 JSON 粘贴到“业务配置”字段,保存后即可使用。 3. 配置完成后即可通过 Swagger 或业务系统调用 `POST /api/sender/publish`,其中 `Receiver`/`Content` 可按各 Sender 提供的格式传入,系统会自动序列化并保存配置。 > `receiver` 和 `content` 字段支持直接传入 JSON 对象/数组,这些字段会转换为字符串后存储,若传入字符串会按原样保存。 ### Sender 配置示例 > 以下 JSON 可直接复制到 Admin 中的“业务配置(ConfigurationJson)”字段即可。 #### FeishuGroupCard (`Type = FeishuGroupCard`) ```jsonc { "card": { "card_id": "AAqhCNs5ZOsJCc", "card_version": "1.0.4" } } ``` - `receiver`:飞书群中,机器人会自动获取 `webhook_url` 和可选 `sign_secret`。 - `content` 作为模板变量 JSON,例如 `{ "alarm": "CPU High" }`。 #### FeishuGroupCommonMessage (`Type = FeishuGroupCommonMessage`) - **必须**在 `ConfigurationJson` 中写入机器人 webhook、签名以及默认 @ 配置;入队时 `receiver` 可以只是 `{}`。 - `receiver` 中提供的字段会与 `ConfigurationJson` 深度合并,同名字段以后者为准,数组(如 `mentions`)会整体替换,可用于临时覆写或清空默认 @。 - `content` 支持结构化对象(如 `{ "title": "...", "content": "..." }`),Sender 会把 `mention_all`/`mentions` 自动拼接到正文最前面,实现 @ 效果。 `ConfigurationJson` 示例: ```jsonc { "webhook_url": "https://open.feishu.cn/...", "sign_secret": "optional", "mention_all": false, "mentions": [ { "id_type": "user_id", "value": "ou_xxx", "name": "值班" } ] } ``` `receiver` 覆盖 mentions 的示例: ```jsonc { "mentions": [ { "id_type": "user_id", "value": "ou_special", "name": "临时值班" } ] } ``` #### FeishuCommonMessage (`Type = FeishuCommonMessage`) ```jsonc { "tenant_access_token": { "base_url": "https://open.feishu.cn", "login_uri": "open-apis/auth/v3/tenant_access_token/internal", "app_id": "cli_xxx", "app_secret": "xxxx" } } ``` - `receiver` 应包含目标 `chat_id`,可选 `msg_type`,例如 `{ "chat_id": "oc_xxx", "msg_type": "text" }`。 - `content` 默认为文本,当 `msg_type` 为 `text` 时,可提供符合官方格式的 JSON 字符串。 #### MockFileMessage (`Type = MockFileMessage`) ```jsonc { "output_path": "MockSender/messages.txt", "append_timestamp": true } ``` - `receiver`/`content` 可自定义;数据会按格式写入本地文件,适合调试。 --- ## 常见问题 | 问题 | 解决 | | --- | --- | | Type 不匹配 | Sender 类型字符串需与 `ISender.Type` 保持一致,可通过 `GET /project/sender-types` 获取可选值。 | | ConfigurationJson 格式错误 | Sender 专用配置,JSON 字符串格式,需符合对应 `ISender` 的要求。 | | Webhook 未触发 | 检查通知规则是否启用并填写合法 URL,查看 `NotificationStatusService` 日志。 | | 签名校验失败 | 确认接收方使用的 secret 与配置一致,且 body 未被修改。 | | 需要扩展 Sender | 实现 `ISender` 并在 `Program.cs` 注册,同时在 `NotificationType` 添加枚举。 | | 队列延迟/不发送 | 检查 `QueueMessageDispatcher` 是否按 `AvailableAt` 正确调度,查看日志排查。 | 若您希望扩展更多能力(例如接入新的 Sender、延迟队列、监控报警等),欢迎在 README 基础上补充说明,以便团队成员和第三方系统更好地理解与集成。 --- ## 通知类型枚举 | 枚举 | 值 | 说明 | | --- | --- | --- | | `FeishuGroupCard` | `8` | 通过自定义机器人发送卡片 Webhook,`receiver` 需包含 `webhook_url`/`sign_secret`,`content` 为模板变量 JSON。 | | `FeishuGroupCommonMessage` | `FeishuGroupCommonMessage` | 自定义机器人文本消息,`ConfigurationJson` 需包含 webhook/sign/默认 @,`receiver` 可覆写相应字段,Sender 会自动拼接 mentions 到正文前。 | | `FeishuCommonMessage` | `16` | 使用开放平台 `chat/v4/send` 接口发送群消息,`receiver` 需包含 `chat_id`,可选 `msg_type`,`content` 默认为文本,也可提供 JSON。 | | `MockFileMessage` | `32` | 测试用本地文本 Sender,`configurationJson` 需指定 `output_path`,数据会写入指定 TXT 文件。 | ### FeishuGroupCommonMessage Receiver 示例 ```jsonc { "mentions": [ { "id_type": "user_id", "value": "ou_xxx", "name": "临时值班" } ] } ``` > `receiver` 只需写“本次要覆盖/增加”的字段,未提供的配置全部继承自 `ConfigurationJson`,Sender 会将 mentions 拼接在文本正文前。 ## SDK(.NET)集成 - 通过 NuGet 安装 `NotifyCenter.SDK.Dotnet`(当前版本 `0.1.6`):`dotnet add package NotifyCenter.SDK.Dotnet`,详见包内 `Readme.md`。 - 在 `Program.cs` / `Startup.cs` 中调用 `services.AddNotifyCenterSenderClient(configuration)` 或 `services.AddNotifyCenterSenderClient(options => { ... })`,配置 `BaseAddress`、`ProjectId`、`ProjectPassword` 后即可通过构造函数注入 `INotifyCenterSenderClient`。 - 调用示例: ```csharp public class DemoService(INotifyCenterSenderClient client) { public Task PublishAsync(object receiver, object content) { return client.EnqueueAsync( notificationId: 20002, receiver: receiver, content: content); } } ``` - 支持自定义 `PublishPath`、`Timeout` 以及 JSON 序列化行为,异常统一抛出 `NotifyCenterSenderException` 并包含 NotifyCenter 返回的 `code/message`。 - SDK 会自动管理 Token:首次调用时通过 `ProjectId + ProjectPassword` 获取 Token 并缓存,Token 过期前会自动续期。 ### 其他语言 SDK - **Node.js**:包名 `@notify-center/sdk-node`,`npm install @notify-center/sdk-node` 后即可 `import { NotifyCenterSenderClient }`。 - **Python**:包名 `notifycenter-sdk-python`,`pip install notifycenter-sdk-python`。 - **Java**:依赖坐标 `com.notifycenter:notifycenter-sdk-java`,支持 Java 17+。 - **Go**:模块 `github.com/notifycenter/notifycenter-sdk-go`,`go get github.com/notifycenter/notifycenter-sdk-go`。 - **PHP**:Composer 包 `notifycenter/sdk-php`。 > 仓库 `sdk/` 目录仅用于查看源码或本地调试,实际业务工程建议直接通过各语言包管理器获取。