跳到主要内容

CmsKit 架构与设计指南

本文档介绍CmsKit模块的架构设计、DDD实现、事件驱动、缓存策略等核心设计决策


🏗️ 整体架构

分层架构

┌─────────────────────────────────────────────────────┐
│ API Controllers │ ← HTTP入口
│ (ArticleController, ShortMsgController, etc) │
└─────────────────────────────────────────────────────┘
↓ 依赖注入
┌─────────────────────────────────────────────────────┐
│ Application Services │ ← 业务编排
│ (ArticleService, CommentService, RecommendService) │
└─────────────────────────────────────────────────────┘
↓ 领域对象
┌─────────────────────────────────────────────────────┐
│ Domain Layer │ ← 业务规则
│ (Article AR, Comment AR, User VO, Permission, etc) │
└─────────────────────────────────────────────────────┘
↓ 持久化
┌─────────────────────────────────────────────────────┐
│ Infrastructure │ ← 技术细节
│ (FreeSql Repositories, Redis, MeiliSearch, etc) │
└─────────────────────────────────────────────────────┘

功能模块分组

CmsKit (Module)

├── Content (内容管理)
│ ├── Article (文章)
│ │ ├── ArticleService
│ │ ├── ArticleVersionService
│ │ ├── ArticleManager (Domain Service)
│ │ └── ArticleController
│ ├── ShortMsg (沸点)
│ │ ├── ShortMsgService
│ │ ├── ShortMsgManager (Domain Service)
│ │ └── ShortMsgController
│ └── Channel (频道)

├── Engagement (社区互动)
│ ├── Comment (评论)
│ │ ├── CommentService
│ │ ├── CommentManager (Domain Service)
│ │ └── CommentController
│ ├── UserLike (点赞)
│ │ ├── UserLikeService
│ │ └── UserLikeController
│ ├── Bookmark (书签)
│ │ ├── BookmarkService
│ │ └── BookmarkController
│ └── Poll (投票)

├── Community (社区社交)
│ ├── User (用户)
│ ├── Club (俱乐部)
│ ├── Notification (通知)
│ └── Feed (动态源)

├── Discovery (内容发现)
│ ├── Search (搜索)
│ ├── Recommendation (推荐)
│ ├── Topic (话题)
│ └── Tag (标签)

└── Governance (治理审核)
├── AuditLog (审计)
├── UserReport (举报)
└── RecycleBin (回收站)

🎯 DDD实现详解

聚合根 (Aggregate Root)

定义: 聚合根是DDD中的重要概念,是聚合的入口点,负责维护聚合内部的一致性。

Article (文章聚合根)

Article (聚合根)
├── ArticleVersion (值对象集合)
│ ├── Title
│ ├── Content
│ ├── Version
│ └── CreateTime
├── ArticleTag (值对象集合)
│ ├── TagId
│ └── TagName
└── ArticleDraft (关联对象)
├── Content
└── LastSaveTime

职责:

  • 维护文章的完整生命周期 (Draft → Pending → Approved → Deleted)
  • 确保版本管理的一致性
  • 管理文章的可见性规则
  • 触发领域事件 (ArticlePublishedEvent, ArticleDeletedEvent等)

关键方法:

public class Article : AggregateRoot
{
// 发布文章
public void Publish(string title, string content, Guid authorId)
{
ValidatePublish();
this.Title = title;
this.Content = content;
this.IsDraft = false;
this.AuditStatus = AuditStatus.Pending;

// 触发事件
AddDomainEvent(new ArticlePublishedEvent(this.Id));
}

// 定时发布
public void SchedulePublish(DateTime publishTime)
{
ValidateSchedulePublish(publishTime);
this.IsScheduled = true;
this.ScheduledPublishTime = publishTime;

AddDomainEvent(new ArticleScheduledEvent(this.Id, publishTime));
}

// 审批通过
public void Approve(Guid reviewerId)
{
if (this.AuditStatus != AuditStatus.Pending)
throw new InvalidOperationException("只有待审核的文章才能批准");

this.AuditStatus = AuditStatus.Approved;
this.ApprovedTime = DateTime.UtcNow;
this.ReviewerId = reviewerId;

AddDomainEvent(new ArticleApprovedEvent(this.Id));
}

// 删除文章
public void Delete()
{
this.IsDeleted = true;
this.DeleteTime = DateTime.UtcNow;

AddDomainEvent(new ArticleDeletedEvent(this.Id));
}
}

ShortMsg (沸点聚合根)

ShortMsg (聚合根)
├── ShortMsgTopic (值对象集合)
│ ├── TopicId
│ └── TopicName
└── Attachments (关联对象集合)
├── AttachmentId
└── Url

Comment (评论聚合根)

Comment (聚合根)
├── SubjectType (被评论内容类型)
├── SubjectId (被评论内容ID)
└── RepliedCommentId (回复的评论ID,可选)

Poll (投票聚合根)

Poll (聚合根)
├── PollOption (值对象集合)
│ ├── OptionId
│ └── Text
└── PollUserVote (关联对象集合)
├── UserId
└── SelectedOptionId

Domain Services (领域服务)

定义: 当操作跨越多个聚合根,或需要外部资源时,使用领域服务。

服务名职责关键方法
ArticleManager文章业务流程编排PublishAsync, ScheduleAsync, AuditAsync
CommentManager评论审核和权限CreateAsync, FilterAsync, ModerateAsync
ShortMsgManager沸点管理PublishAsync, AuditBatchAsync
ArticleHotIndexService热度计算CalculateAsync, UpdateAsync
RecommendationManager推荐算法GetRecommendationsAsync
MentionService@提及解析ParseMentionsAsync, NotifyAsync

🔄 事件驱动架构

领域事件 (Domain Event)

定义: 领域事件表示在业务中发生的、具有业务意义的事件。

事件分类

领域事件
├── 内容事件
│ ├── ArticlePublishedEvent
│ ├── ArticleApprovedEvent
│ ├── ArticleDeletedEvent
│ ├── ShortMsgPublishedEvent
│ └── ShortMsgDeletedEvent
├── 互动事件
│ ├── UserLikedEvent
│ ├── CommentCreatedEvent
│ ├── BookmarkAddedEvent
│ └── UserFollowedEvent
├── 系统事件
│ ├── AuditLogCreatedEvent
│ ├── UserReportedEvent
│ └── NotificationSentEvent
└── 跨模块事件
├── UserCreatedEvent (来自Identity)
├── FileUploadedEvent (来自File)
└── UserBlockedEvent (来自Identity)

事件发布示例

// 1. 定义事件
public class ArticlePublishedEvent : DomainEvent
{
public Guid ArticleId { get; set; }
public string Title { get; set; }
public Guid AuthorId { get; set; }
public DateTime PublishedTime { get; set; }
}

// 2. 在聚合根中发布事件
public class Article : AggregateRoot
{
public void Publish(string title, string content, Guid authorId)
{
// 业务逻辑...

// 发布领域事件
AddDomainEvent(new ArticlePublishedEvent
{
ArticleId = this.Id,
Title = title,
AuthorId = authorId,
PublishedTime = DateTime.UtcNow
});
}
}

// 3. 在应用服务中处理事件
public class ArticleService : IArticleService
{
private readonly IMediator _mediator;

public async Task<ArticleDto> PublishAsync(CreateArticleReq req)
{
var article = Article.Create(req.Title, req.Content, CurrentUserId);
article.Publish(req.Title, req.Content, CurrentUserId);

await _articleRepository.AddAsync(article);

// MediatR 会自动发布和处理所有领域事件
await _unitOfWork.SaveEntitiesAsync();

return new ArticleDto { Id = article.Id };
}
}

// 4. 定义事件处理器
public class ArticlePublishedEventHandler : INotificationHandler<ArticlePublishedEvent>
{
private readonly ILogger<ArticlePublishedEventHandler> _logger;
private readonly INotificationService _notificationService;

public async Task Handle(ArticlePublishedEvent evt, CancellationToken cancellationToken)
{
_logger.LogInformation($"文章已发布: {evt.Title}");

// 处理1: 同步MeiliSearch
await _meiliSearchService.IndexArticleAsync(evt.ArticleId);

// 处理2: 发送通知给订阅用户
await _notificationService.NotifyFollowersAsync(evt.AuthorId,
$"你关注的作者发布了新文章: {evt.Title}");

// 处理3: 更新作者统计
await _authorStatsService.IncrementPublishedCountAsync(evt.AuthorId);
}
}

事件处理流程

用户发布文章

ArticleService.PublishAsync()
├─ 创建Article聚合根
├─ 调用 article.Publish()
│ └─ AddDomainEvent(ArticlePublishedEvent)
├─ 保存到数据库
└─ 触发UnitOfWork.SaveEntitiesAsync()

发布所有待处理的领域事件
├─ ArticlePublishedEventHandler
│ └─ 同步MeiliSearch、发送通知、更新统计
├─ AuditLogEventHandler
│ └─ 记录审计日志
└─ 其他事件处理器...

所有处理器执行完成

返回结果给用户

💾 缓存策略

缓存分层

┌─────────────────────────────────────┐
│ 本地内存缓存 (Memory) │ ← L1: 进程内缓存
│ TTL: 5分钟, 容量: 1000个对象 │
└─────────────────────────────────────┘
↓ Miss
┌─────────────────────────────────────┐
│ 分布式缓存 (Redis) │ ← L2: 共享缓存
│ TTL: 1小时, 容量: 100万个对象 │
└─────────────────────────────────────┘
↓ Miss
┌─────────────────────────────────────┐
│ 数据库 (FreeSql) │ ← L3: 持久存储
└─────────────────────────────────────┘

缓存key设计

缓存类型Key格式TTL更新策略
文章article:{articleId}24h编辑时失效
文章列表articles:hot:{category}1h每小时更新
用户user:{userId}24h信息修改时失效
热度hotindex:{articleId}1h互动时失效
推荐recommendation:{userId}2h用户行为时更新
搜索建议search:suggestions:{keyword}7d定期刷新

缓存实现示例

public class ArticleService : IArticleService
{
private readonly IRepository<Article> _repository;
private readonly IDistributedCache _cache;
private readonly IMemoryCache _memoryCache;

// 三级缓存读取
public async Task<ArticleDto> GetArticleAsync(Guid articleId)
{
var cacheKey = $"article:{articleId}";

// 1. 本地内存缓存
if (_memoryCache.TryGetValue(cacheKey, out ArticleDto cachedArticle))
{
return cachedArticle;
}

// 2. Redis分布式缓存
var redisCached = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(redisCached))
{
var article = JsonSerializer.Deserialize<ArticleDto>(redisCached);
// 回写到本地缓存
_memoryCache.Set(cacheKey, article, TimeSpan.FromMinutes(5));
return article;
}

// 3. 数据库查询
var articleEntity = await _repository.GetAsync(articleId);
var articleDto = MapToDto(articleEntity);

// 写入两级缓存
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(articleDto),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) });
_memoryCache.Set(cacheKey, articleDto, TimeSpan.FromMinutes(5));

return articleDto;
}

// 缓存失效
public async Task UpdateArticleAsync(UpdateArticleReq req)
{
var article = await _repository.GetAsync(req.ArticleId);
article.Update(req.Title, req.Content);

await _repository.UpdateAsync(article);

// 清除所有相关缓存
var cacheKey = $"article:{article.Id}";
_memoryCache.Remove(cacheKey);
await _cache.RemoveAsync(cacheKey);

// 清除列表缓存
await _cache.RemoveAsync($"articles:hot:*");
}
}

缓存预热

// 系统启动时预热热数据
public class CacheWarmupService : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
// 1. 预热热排文章
var hotArticles = await _articleRepository
.Where(a => a.AuditStatus == AuditStatus.Approved && !a.IsDeleted)
.OrderByDescending(a => a.HotIndex)
.Take(100)
.ToListAsync();

foreach (var article in hotArticles)
{
var cacheKey = $"article:{article.Id}";
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(MapToDto(article)),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) });
}

// 2. 预热推荐数据
await _recommendationService.WarmupCacheAsync();

_logger.LogInformation("缓存预热完成");
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

🔌 跨模块集成

模块依赖关系

CmsKit
├─ 依赖 Identity
│ ├─ 验证用户身份
│ ├─ 获取用户信息 (头像、昵称)
│ └─ 监听 UserUpdatedEvent

├─ 依赖 Identity.Infrastructure
│ ├─ 公共附件存储
│ ├─ 图片缩略图
│ └─ 设置管理

├─ 依赖 Job
│ ├─ 后台任务 (MeiliSearch同步)
│ ├─ 定时任务 (定时发布)
│ └─ 异步处理

└─ 依赖 Platform
├─ 通知中心
├─ SignalR推送
└─ 消息队列(CAP/RabbitMQ)

事件驱动集成

// 监听来自Identity的事件
public class IdentityEventHandler :
INotificationHandler<UserProfileUpdatedEvent>
{
private readonly IRepository<CmsUser> _userRepository;

public async Task Handle(UserProfileUpdatedEvent evt, CancellationToken ct)
{
// 同步用户信息到CMS
var cmsUser = await _userRepository.GetAsync(evt.UserId);
if (cmsUser != null)
{
cmsUser.Avatar = evt.NewAvatar;
cmsUser.DisplayName = evt.NewDisplayName;
await _userRepository.UpdateAsync(cmsUser);

// 清除缓存
await _cache.RemoveAsync($"user:{evt.UserId}");
}
}
}

// 发送事件到其他模块
public class ArticleService
{
private readonly IEventPublisher _eventPublisher;

public async Task PublishAsync(CreateArticleReq req)
{
var article = CreateArticle(req);
await _articleRepository.AddAsync(article);

// 发布跨模块事件
await _eventPublisher.PublishAsync(new ArticlePublishedIntegrationEvent
{
ArticleId = article.Id,
AuthorId = article.CreateUserId,
Title = article.Title,
Content = article.Content,
PublishedTime = DateTime.UtcNow
});
}
}

🏢 Module Startup 配置

依赖注入注册

public class CmsKitModuleStartup : IModuleStartup
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
// 1. 注册仓储
services.AddScoped(typeof(IBaseRepository<>), typeof(GRepository<>));
services.AddScoped<IArticleRepository, ArticleRepository>();

// 2. 注册应用服务
services.AddScoped<IArticleService, ArticleService>();
services.AddScoped<ICommentService, CommentService>();

// 3. 注册领域服务
services.AddScoped<IArticleManager, ArticleManager>();
services.AddScoped<ICommentManager, CommentManager>();

// 4. 注册后台任务
services.AddScoped<IScheduledPublishJob, ScheduledPublishJob>();
services.AddScoped<IMeiliSearchSyncJob, MeiliSearchSyncJob>();

// 5. 注册工具服务
services.AddScoped<IHotIndexService, HotIndexService>();
services.AddScoped<IMentionService, MentionService>();

// 6. 注册Hub
services.AddScoped<CmsKitNotificationHub>();

// 7. 配置选项
services.Configure<CmsKitOptions>(configuration.GetSection("CmsKit"));

// 8. 注册事件处理器
services.AddMediatRHandlers();
}
}

🔐 安全设计

权限校验策略

// 基于属性的权限检查
[ApiController]
[Route("api/cms/articles")]
public class ArticleController
{
[HttpPost]
[Authorize]
[RequirePermission("article:create")]
public async Task<IActionResult> CreateAsync([FromBody] CreateArticleReq req)
{
// 仅具有 article:create 权限的用户可执行
return Ok(await _articleService.CreateAsync(req));
}

[HttpPut("{id}")]
[Authorize]
[RequirePermission("article:edit")]
public async Task<IActionResult> UpdateAsync(Guid id, [FromBody] UpdateArticleReq req)
{
return Ok(await _articleService.UpdateAsync(id, req));
}
}

// 数据级别的权限检查
public async Task<ArticleDto> GetArticleAsync(Guid articleId)
{
var article = await _articleRepository.GetAsync(articleId);

// 检查用户是否有权限访问该文章
if (!await _permissionService.CanAccessArticleAsync(article, CurrentUserId))
throw new ForbiddenException("无权限访问");

return MapToDto(article);
}

输入验证

public class CreateArticleReq
{
[Required]
[StringLength(200, MinimumLength = 1)]
public string Title { get; set; }

[Required]
[StringLength(50000, MinimumLength = 1)]
public string Content { get; set; }

[Range(0, 10)]
public int CategoryId { get; set; }
}

// 自定义验证器
public class CreateArticleReqValidator : AbstractValidator<CreateArticleReq>
{
public CreateArticleReqValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("标题不能为空")
.Length(1, 200).WithMessage("标题长度在1-200之间");

RuleFor(x => x.Content)
.NotEmpty().WithMessage("内容不能为空")
.Length(1, 50000).WithMessage("内容长度在1-50000之间");
}
}

📊 监控与可观测性

结构化日志

public class ArticleService
{
private readonly ILogger<ArticleService> _logger;

public async Task<ArticleDto> PublishAsync(CreateArticleReq req)
{
_logger.LogInformation(
"用户发布文章 | UserId: {UserId} | Title: {Title} | Categories: {Categories}",
CurrentUserId,
req.Title,
string.Join(",", req.CategoryIds));

try
{
var article = await _articleRepository.AddAsync(CreateArticle(req));

_logger.LogInformation(
"文章发布成功 | ArticleId: {ArticleId} | UserId: {UserId}",
article.Id,
CurrentUserId);

return MapToDto(article);
}
catch (Exception ex)
{
_logger.LogError(ex,
"文章发布失败 | UserId: {UserId} | Title: {Title}",
CurrentUserId,
req.Title);

throw;
}
}
}

性能指标

// 使用Prometheus导出指标
public class MetricsMiddleware
{
private static readonly Counter ArticlePublishedCounter =
Counter.Create("cms_article_published_total", "已发布的文章总数");
private static readonly Gauge ActiveUsersGauge =
Gauge.Create("cms_active_users", "活跃用户数");
private static readonly Histogram RequestDurationHistogram =
Histogram.Create("cms_request_duration_ms", "请求延迟",
new[] { 0, 10, 50, 100, 500, 1000 });

public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();

await _next(context);

stopwatch.Stop();
RequestDurationHistogram.Observe(stopwatch.ElapsedMilliseconds);
}
}

📚 相关文档


最后更新: 2026-05-10