跳到主要内容

CmsKit 内容社区模块

CmsKit 是一个以**内容(文章 / 沸点)+ 互动(评论 / 投票 / 点赞 / 收藏 / 关注)+ 社区(圈子 / 用户 / 通知)+ 发现(搜索 / 推荐 / 话题 / 标签)**为核心的可复用业务模块。它建立在 Identity.Infrastructure 提供的文件存储、设置系统、通用附件等横切能力之上,并集成 MeiliSearch 做全文检索、SignalR 做实时通知、CAP 事件总线做跨服务解耦。

本指南面向想理解这个模块的开发者:它的领域模型、概念之间如何关联、为什么这样设计、每个部分承载哪些业务能力。它不是 API 参考手册——端点的完整清单请直接看 Swagger(cms / cms-admin 两个分组),本文不罗列方法/路由表。

模块边界与子域划分

CmsKit 的边界是「以 UGC 内容为中心的社区能力」。它负责:底层身份与鉴权(交给 Identity 模块)、文件字节存储(交给 Identity.Infrastructure 的文件中心 KitFile)、租户与跨模块设置基础设施(同样来自宿主)。CmsKit 只定义「社区业务有哪些权限」「内容长什么样」「互动与发现怎么发生」。

按业务能力,模块在代码上(Application/ 目录)拆成五个子域,外加一层横切基础设施:

子域职责代表实体 / 服务边界说明
Content(内容)文章、沸点、频道、分类、草稿、定时发布、SEOArticle / ShortMsg / ArticleDraft / Classify / Channel / IArticleService内容的「生产侧」,不直接处理点赞评论
Engagement(互动)评论、投票、收藏、点赞/踩、微反应、关注、互动日志Comment / UserLike / Bookmark / Poll / Boost / UserSubscribe挂载在内容之上,与具体子域解耦
Community(社区)圈子与角色、CMS 用户、在线访客、RSS、通知Club / ClubMemberRole / CmsUser / Notification把人和内容组织成「社群」
Discovery(发现)搜索、推荐、话题、标签、作者中心、统计Topic / Tag / IArticleRecommendationService / ISearchSuggestionService解决「内容怎么被找到」
Governance(治理)审核状态流转、举报、附件同步IEntityAuditLogManager / IUserReportService偏管理端的内容合规与生命周期
基础设施(横切)MeiliSearch 索引、SignalR 通知、CAP 事件、配置IArticleMeiliSearchService / NotificationHub / CmsKitJobService被各子域复用,不独立对外

为什么这样划边界:Content 关注「内容本体」,Engagement 关注「人对内容的反应」,二者通过后文提到的 SubjectId/SubjectType 关联而不是互相引用——这让点赞、收藏、评论可以无差别地作用到文章或沸点上,新增内容形态时互动层几乎不用改。Community 和 Discovery 则是围绕「人」和「分发」的两个正交维度。

核心概念关系

下图是 CmsKit 领域模型的主干关系(所有连线均来自真实代码中的外键 / 导航属性):

几点从代码可验证的关系事实:

  • 内容 → 载体Article 通过 ChannelId 挂到频道、ClassifyId 挂到分类专栏(见 Domain/Articles/Article.cs 的导航属性)。ShortMsg 通过 ClubId 可选挂到圈子、PollId 可选挂到投票。
  • 文章 → 草稿Article.ArticleDraft 是一一对应的草稿箱(cms_article_draft),与正式文章物理分离。
  • 互动与内容的联结:评论、点赞、收藏项都用 SubjectId + 一个形态枚举(CommentSubjectType / BookmarkSubjectType / UserLikeSubjectType)指向「被作用的内容」,而不是分别建 CommentForArticle / CommentForShortMsg 两张表。
  • 跨形态引用(Quote/Repost)ShortMsg 自带 QuotedSubjectId + QuotedSubjectKindPostKind 区分文章/沸点),并在转贴瞬间把原帖快照(作者、正文前 200 字、首图、时间)固化下来——原帖后续编辑不影响已转内容。
  • 圈子 → 成员ClubCmsUser 通过 ClubMemberRole(含 ClubRoleType 角色)多对多联结,圈子是社区组织的核心聚合根(Club : AggregateRoot)。

设计动机(why)

草稿与正式文章分离

ArticleDraft 是独立表(Domain/Articles/ArticleDraft.cs),编辑器自动保存只写草稿箱,绝不会触碰已发布的 Article。原因很直接:自动保存是高频、易中断的操作,不能让它污染线上内容或触发索引/通知。发布时再由草稿生成/更新正式文章。

定时发布走后台任务而非请求链路

文章设置 IsScheduled + ScheduledPublishTime 后,并不是在用户请求里直接上线,而是由后台作业 CmsKitJobService.ScheduledPublishAsync 定时扫描 IsScheduled && ScheduledPublishTime <= now && AuditStatus==Approved 的文章批量发布(置 IsDraft=false、写 PublishedTime、同步 MeiliSearch、给作者发系统通知)。这样设计的好处是:发布动作与「作者点击那一刻」解耦,即便应用在发布点短暂不可用,到点后仍能被下一轮扫描补发,且不会阻塞作者端的写请求。

内容检索走 MeiliSearch,且以「轮询增量」为主

文章/沸点在创建或更新后,并不依赖某个领域事件实时写索引,而是由后台作业 IncrementalSyncMeiliSearchAsync 每 5 分钟扫描一次 UpdateTime 窗口内、满足「公开 + 审核通过 + 未软删」的内容并增量同步(软删除的内容则从索引移除)。文章索引按 ArticleCategory 拆成 articles_Normal / articles_AiNews,沸点为单一 shortmsgs 索引。选择轮询而非事件驱动,是为了在写入吞吐与检索新鲜度之间取平衡,并避免主写入链路被搜索引擎拖慢;代价是新内容最多有约 5 分钟的检索延迟。

通知用 CAP 事件总线解耦

当有人评论你的文章、给你点赞、关注你时,CommentService / UserLikeService / UserSubscribeService 等只通过 capBus.PublishAsync(CreateNotificationReq.CreateOrCancelAsync, ...) 发一条事件,真正的落库与推送由消费方 NotificationService.CreateOrCancelAsync 完成,并经 SignalR NotificationHub 推给在线用户。请求方不等待通知写入完成。该消息还支持「创建 / 撤销」两种语义(IsCancel),例如取消点赞会发一条撤销消息把对应通知移除——天然实现幂等与去重。

业务能力地图

子域典型业务能力(不是端点)
Content发布/编辑 Markdown 文章;编辑器自动保存草稿;定时发布;按分类/频道组织;置顶;定时/实时计算热门榜;AI 提取 SEO(关键词/摘要/标签);HTML→Markdown 导入
Content(沸点)发布轻量短内容;配图/话题/投票;公开或仅自己可见;引用转发(带原帖快照);草稿;自动转短链
Engagement多级评论(系统/用户/内容三重可评论校验);单选/多选投票;收藏夹+收藏项;点赞/踩;介于点赞与评论之间的 Micro-reaction(Boost);用户间关注/粉丝;用户标签订阅;互动行为日志
Community圈子广场/热门/推荐;成员角色(圈主/管理员/成员)与权限(加入/退出/转让/封禁);CMS 用户主页与统计;在线访客提示;RSS/Atom 订阅源;实时通知中心
DiscoveryMeiliSearch 全文检索、搜索建议、热搜、历史、高亮;个性化推荐流(文章/沸点 + 相关推荐);话题与标签聚合;作者中心统计;全站站点统计;AI 资讯流
Governance内容审核状态流转(通过/拒绝/拉黑);用户举报受理;附件同步到文件中心

快速开始

CmsKit 以 IModuleStartup 形式接入宿主,不需要 AddFreeKitCmsKit() 之类的扩展方法(旧文档中的该写法是不存在的)。

  1. 在宿主的模块映射表 src/Services/Host/FreeKit.DI/E.cs 中确认已登记:

    public static readonly Dictionary<string, Type> Modules = new(StringComparer.OrdinalIgnoreCase)
    {
    // ...
    { "cmskit", typeof(CmsKitModuleStartup) },
    };
  2. Program.cs 中通过通用入口加载全部模块(宿主已默认如此):

    builder.UseFreeKit(
    moduleTypeMap: E.Modules,
    extraAssemblies: new[] { typeof(Program).Assembly }
    );
    // ...
    app.ConfigureModules().Init();
  3. 启动后自动完成:绑定 FileStorage / Site 配置、注册 SignalR 与 NotificationHub/hubs/notifications)、注册各子域服务、构造 MeilisearchClient(地址来自运行时设置,见下)。DEBUG 下还会 CodeFirst.SyncStructure 同步表结构。

关闭模块:把 E.Modules 字典里 "cmskit" 这一项去掉(或整个字典只保留需要的模块),宿主就不会加载 CmsKit 的全部服务与路由。模块是「注册即存在」,没有独立的运行时开关。

配置(讲业务含义)

CmsKit 的配置分两部分:

  • 启动时绑定(appsettings)FileStorageSite 两节通过 services.Configure<...>(configuration.GetSection(...)) 绑定。
  • 运行时设置(数据库 Settings 表,可热更新):绝大多数开关通过 ICmsKitRuntimeSettingProvider 从系统设置读取,键名定义在 CmsKitConstCmsKitSettingDefinitionProvider 中登记)。这些键在后台「系统设置」里修改即生效,无需改 appsettings 或重启
设置键业务含义(不是端点)
MeiliSearch.Host / MeiliSearch.ApiKeyMeiliSearch 地址与密钥。两者都可为空——为空时回落到本机 http://127.0.0.1:7700源码没有独立 Enable 开关:只要地址可达,检索即可用。生产环境建议用 Search-only Key 给前端直连
Search.ArticleSuggestionScore / Search.ShortMsgSuggestionScore搜索建议进入候选的阈值分,调高可让建议更「精」、调低更「全」
HotIndex.TotalRounds / HotIndex.BatchSize / HotIndex.RecentMsgThreshold热度计算的轮次、批处理大小与时间窗口,决定热门榜更新的频率与开销
Article.Commentable文章「是否可评论」的全局默认开关(内容级仍可被作者单独覆盖)
Article.AuditStatus.Default / ShortMsg.AuditStatus.Default / Comment.AuditStatus.Default三类内容发布后的默认审核状态(通过/待审/拒绝),是审核策略的总闸
AI.ContentAnalysisAI 提取 SEO 的配置(JSON),为空则 ExtractSeo 不可用
Host.IdentityApi / Host.ShortUrlApi关联的 Identity / 短链服务地址,用于跨模块跳转与短链生成
Site.ApiDomain站点 API 域名,用于生成外链、RSS 等绝对地址
ShortMsg.AutoShortUrl.Enabled / ExternalLink.WhitelistDomains / ExternalLink.HintText沸点正文里的外链是否自动转短链、白名单域名、点击外链时的风险提示文案

这些键通过 ISettingManager.GetSystemOrNullAsync(name) 读取,与 Identity 的 Setting 体系一致。建议通过后台「系统设置」界面或 Identity 设置 API 写入,不要在业务代码里硬编码地址。

功能子页