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