跳到主要内容

Platform 架构设计

Platform 聚合了大量「工具型」子域(待办、短链接、热榜、TTS、导航、消息、节假日……)。它使用 IModuleStartup 统一接入宿主,但内部不是一套完全整齐的 DDD 模板:Holiday、ShortUrl、ToDo 是垂直切片,其他能力主要位于 Application/。本文按当前代码说明真实边界与几个关键取舍。

模块启动协议

Platform 通过 PlatformModuleStartup : IModuleStartup 挂载到主宿主(见 宿主总览)。所有子域的注册集中在 ConfigureServicesConfigure 中在 DEBUG 下用 CodeFirst.SyncStructure(...) 同步各子域表结构。

public class PlatformModuleStartup : IModuleStartup
{
public void ConfigureServices(IServiceCollection services, IConfiguration c)
{
// 子域服务注册(见下文)
}

public void Configure(WebApplication app, IWebHostEnvironment env)
{
// DEBUG 下 CodeFirst.SyncStructure(...)
}
}

src/Services/Host/FreeKit.DI/E.csModules 字典供 Job、Message 等后台宿主加载 Platform 核心模块。主 API 还会在 FreeKit.Host/Program.cs 显式注册 PlatformModuleStartup,并把 hostModules["platform"] 替换成 PlatformHttpApiModuleStartup。因此模块启停必须同时调整这两个组合入口;只删除字典条目不会从主 API 完整移除 Platform。

为什么是 IModuleStartup 而不是独立微服务:这些子域之间几乎不需要跨域事务,又常被业务复用。进程内模块边界既保留了「可整体关闭/替换」的边界感,又省下远程调用的复杂度与运维成本。

分层结构

FreeKit.Platform/
├── Application/ # 应用层:用例、DTO、服务
│ ├── DailyHot/ # 热榜聚合(多平台 Provider)
│ ├── Nav/ # 导航分类与导航项
│ ├── Messages/ # 消息中心(多渠道发送)
│ ├── Notices/ # 通知
│ ├── Projects/ # 开发者项目
│ ├── TTS/ # 文本转语音(MiMo)
│ └── ...
├── ShortUrl/ # 短链接子域(独立 FreeSql 数据源)
├── Holiday/ # 节假日子域(独立目录分层)
│ ├── Application/ Domain/ Infrastructure/
└── ToDo/ # 待办子域
└── Application/ # INotificationScheduler + FreeScheduler Adapter

各子域的成熟度不同。新增代码应采用「Application Service → IAuditBaseRepository<T> / Domain Manager → FreeSql」的边界;Holiday 等目录仍保留 Application 直连 IFreeSql 的历史债务。HTTP Controller 已迁入 FreeKit.Platform.HttpApi,核心项目不应再新增 Controller。

子域注册与关键设计取舍

所有子域的服务都在 PlatformModuleStartup.ConfigureServices 中集中注册。下面三类注册最能体现 Platform 的架构取舍。

1. 短链接:独立数据源而非共用主库

短链接使用独立 FreeSql 实例(marker 类型 ShortFlag,连接串 ShortUrlConnectionStrings),与主库物理隔离:

services.AddDefaultFreeSql<ShortFlag>(c, "ShortUrlConnectionStrings");
services.TryAddScoped(typeof(IShortDbRepository<,>), typeof(ShortDbRepository<,>));
services.AddShortUrlServices(c);

为什么:短链通常写入量高、读多且体量大,且业务上应与核心数据解耦。用独立 IFreeSql 实例 + UnitOfWorkManager<ShortFlag>,让短链库可单独运维、备份、分表而不影响主业务库。其仓储 IShortDbRepository 也单独定义,避免与默认仓储串库。

2. 消息中心:策略模式屏蔽渠道差异

消息发送抽象为 IMessageChannelSender(飞书/企业微信/公众号各一个实现),由 PlatformMessageServiceMessageType 路由;新增渠道只是再加一个实现,不动核心发送逻辑:

services.AddScoped<IPlatformMessageService, PlatformMessageService>();
services.AddScoped<IMessageChannelSender, FeishuWebhookMessageChannelSender>();
services.AddScoped<IMessageChannelSender, WeComWebhookMessageChannelSender>();
services.AddScoped<IMessageChannelSender, WeChatOfficialAccountMessageChannelSender>();

为什么:飞书 Webhook、企微 Webhook、公众号模板消息的鉴权与报文各不相同。PlatformMessageService 把“一条消息 → 多个渠道 → 各自落库记录”的统一流程稳定下来,把“如何发到某个平台”的易变点收敛到策略实现里,并以 Success / Failed / Skipped 记录结果。当前 RetryAsync 是显式重试入口,并非后台自动重试队列。

3. 热榜聚合:Keyed Service 多实现,可插拔

当前有 27 个 IDailyHotService 实现通过 AddKeyedSingletonDailyHotEnum 键注册;聚合 Job 用 GetRequiredKeyedService<IDailyHotService>(key) 逐个取用:

services.AddKeyedSingleton<IDailyHotService, ZhiHuService>(DailyHotEnum.ZhiHu);
services.AddKeyedSingleton<IDailyHotService, BiliBiliService>(DailyHotEnum.BiliBili);
// ... 其余平台

为什么:每个平台的数据源、字段、反爬策略都不同,但对外暴露的“拿热榜”契约一致。BaseDailyHotService 统一了 IHttpClientFactory 拉取与 Redis 缓存(120 分钟 TTL)的样板,DailyHotJobService 负责把结果落库到 DailyHotItem。这些客户端目前使用未命名的 HttpClient,尚未像 TTS 一样配置专用弹性管线。

4. TTS:独立弹性管线,避免慢调用拖垮全局

MiMo TTS 合成较慢,单独配置 HttpClient 弹性管线(按 MimoTtsOptions.TimeoutSeconds 给足单次超时,并对瞬时故障重试、对慢超时熔断):

services.AddHttpClient<IMimoTtsClient, MimoTtsClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<MimoTtsOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/");
client.Timeout = Timeout.InfiniteTimeSpan; // 超时交给弹性管线,避免双重计时
})
.AddStandardResilienceHandler(options =>
{
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(ttsTimeoutSec);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(ttsTimeoutSec + 10);
options.Retry.MaxRetryAttempts = 2;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(ttsTimeoutSec * 2);
});

为什么:TTS 单次合成可能远超普通 HTTP 请求的耗时。若共用全局超时,要么整体超时预算被 TTS 拉爆、要么 TTS 永远超时失败。把它单独隔离出弹性管线,既给足时间又用熔断保护 MiMo 侧不被重复慢请求打爆。

5. 待办提醒:用调度器 seam 解耦通知渠道

待办的到点提醒声明一个 INotificationScheduler 接口,当前实现 FreeSchedulerNotificationAdapter 基于 FreeScheduler 登记一次性任务:

services.AddScoped<INotificationScheduler, FreeSchedulerNotificationAdapter>();

当前只有更新待办时,未来的 NotificationTime 才会调用 ScheduleNotification(todoId, notificationTime);创建路径只保存实体,不会登记任务。Adapter 使用 AddTask(..., delaySeconds, -1) 建立一次性任务。Job Host 的处理器到点后重新校验待办状态,标记已提醒,再调用 CmsKit 的 INotificationService 写入站内通知。

为什么:接口把“何时提醒”与“如何产生站内通知”分开,核心待办服务不需要直接依赖 Job Host。不过创建时未调度、更新后重复调度及本地时间计算仍是当前实现需要继续收敛的边界,详见待办管理

如何新增一个工具子域

  1. 在核心项目的 Application/(或独立目录如 Holiday/ShortUrl/)定义实体、Domain Manager 与应用服务;Controller 放入 FreeKit.Platform.HttpApi 的对应目录。
  2. PlatformModuleStartup.ConfigureServices 注册服务;如有独立表,在 ConfigureSyncStructure 中追加实体类型。
  3. 需要独立数据源时,仿照短链接使用 AddDefaultFreeSql<T> + 独立连接串。
  4. 确认核心服务由 PlatformModuleStartup 注册、HTTP Controller 由 PlatformHttpApiModuleStartup 所在程序集发现;不要只修改 E.Modules

相关文档