Platform 架构设计
Platform 聚合了大量「工具型」子域(待办、短链接、热榜、TTS、导航、消息、节假日……)。它使用 IModuleStartup 统一接入宿主,但内部不是一套完全整齐的 DDD 模板:Holiday、ShortUrl、ToDo 是垂直切片,其他能力主要位于 Application/。本文按当前代码说明真实边界与几个关键取舍。
模块启动协议
Platform 通过 PlatformModuleStartup : IModuleStartup 挂载到主宿主(见 宿主总览)。所有子域的注册集中在 ConfigureServices,Configure 中在 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.cs 的 Modules 字典供 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(飞书/企业微信/公众号各一个实现),由 PlatformMessageService 按 MessageType 路由;新增渠道只是再加一个实现,不动核心发送逻辑:
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 实现通过 AddKeyedSingleton 按 DailyHotEnum 键注册;聚合 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。不过创建时未调度、更新后重复调度及本地时间计算仍是当前实现需要继续收敛的边界,详见待办管理。
如何新增一个工具子域
- 在核心项目的
Application/(或独立目录如Holiday/、ShortUrl/)定义实体、Domain Manager 与应用服务;Controller 放入FreeKit.Platform.HttpApi的对应目录。 - 在
PlatformModuleStartup.ConfigureServices注册服务;如有独立表,在Configure的SyncStructure中追加实体类型。 - 需要独立数据源时,仿照短链接使用
AddDefaultFreeSql<T>+ 独立连接串。 - 确认核心服务由
PlatformModuleStartup注册、HTTP Controller 由PlatformHttpApiModuleStartup所在程序集发现;不要只修改E.Modules。