架构设计 (Architecture)
本文档详细介绍文章模块的架构设计、设计模式和技术决策,帮助你深入理解模块的内部实现。
🏗️ 整体架构
分层架构
文章模块采用DDD分层架构 + 设计模式优化,实现清晰的职责分离和高内聚低耦合:
graph TB
subgraph "API层 (blog-article-api)"
DTO[ArticleDTO]
VO1[ArticleDetailVO]
VO2[ArticleListVO]
Interface[IArticleService]
end
subgraph "Service层 (blog-article-service)"
Controller1[ArticleController]
Controller2[ArticleAdminController]
Service[ArticleServiceImpl]
subgraph "Domain Layer"
Entity[ArticleEntity]
State[State Pattern]
Event[Spring Event]
end
subgraph "Infrastructure"
Mapper[ArticleMapper]
Converter[ArticleConverter]
Chain[Content Processor Chain]
Vector[Vector Search]
end
end
subgraph "外部服务"
DB[(MySQL 9.4 VECTOR)]
Embedding[Embedding API]
end
Controller1 --> Service
Controller2 --> Service
Service --> State
Service --> Chain
Service --> Event
Service --> Mapper
Service --> Vector
Mapper --> Entity
Entity --> DB
Vector --> Embedding
style State fill:#ffe8e8
style Chain fill:#e8f5e9
style Event fill:#e1f5ff
style Vector fill:#fff4e1
层级职责
| 层 | 职责 | 关键组件 |
|---|---|---|
| API层 | 定义契约(DTOs/VOs/Interfaces) | ArticleDTO, ArticleListVO |
| Controller层 | HTTP请求处理,参数验证 | ArticleController, ArticleAdminController |
| Service层 | 业务逻辑实现 | ArticleServiceImpl + State + Chain + Event |
| Domain层 | 领域模型,业务规则 | ArticleEntity, ArticleState, ArticlePublishedEvent |
| Infrastructure层 | 技术实现,外部集成 | ArticleMapper, VectorSearchService |
🎨 三大设计模式详解
1. State Pattern (状态模式) ⭐⭐⭐
问题分析
文章有三种状态:草稿 (DRAFT)、已发布 (PUBLISHED)、已归档 (ARCHIVED)
不同状态下允许的操作完全不同:
- 草稿:可发布、可删除、不能归档
- 已发布:可归档、可删除、不能再发布
- 已归档:可恢复、可删除、不能归档
如果用传统if-else实现:
// ❌ 传统实现(代码臭味)
public void publishArticle(Long id) {
ArticleEntity article = articleMapper.selectById(id);
if (article.getStatus() == ArticleStatus.DRAFT.getCode()) {
// 草稿可以发布
article.setStatus(ArticleStatus.PUBLISHED.getCode());
article.setPublishTime(LocalDateTime.now());
articleMapper.updateById(article);
} else if (article.getStatus() == ArticleStatus.PUBLISHED.getCode()) {
throw new BusinessException("文章已发布,不能重复发布");
} else if (article.getStatus() == ArticleStatus.ARCHIVED.getCode()) {
throw new BusinessException("已归档文章不能发布,请先恢复");
}
}
public void archiveArticle(Long id) {
ArticleEntity article = articleMapper.selectById(id);
if (article.getStatus() == ArticleStatus.DRAFT.getCode()) {
throw new BusinessException("草稿不能归档");
} else if (article.getStatus() == ArticleStatus.PUBLISHED.getCode()) {
// 已发布可以归档
article.setStatus(ArticleStatus.ARCHIVED.getCode());
articleMapper.updateById(article);
} else if (article.getStatus() == ArticleStatus.ARCHIVED.getCode()) {
throw new BusinessException("文章已归档");
}
}
// 每个状态操作都要重复这些 if-else,代码重复且难以维护!
问题:
- ❌ 代码重复(每个方法都要判断所有状态)
- ❌ 违反开闭原则(新增状态需要修改所有方法)
- ❌ 逻辑分散(一个状态的规则散落在多个方法中)
State Pattern 解决方案
核心思想: 将状态相关的行为封装到独立的状态类中,状态转换由状态对象自己负责。
类图设计:
classDiagram
class ArticleState {
<<interface>>
+publish(article) void
+archive(article) void
+unarchive(article) void
+canDelete() boolean
}
class DraftState {
-INSTANCE: DraftState
+publish(article) ✅
+archive(article) ❌ throws Exception
+unarchive(article) ❌ throws Exception
+canDelete() ✅ true
}
class PublishedState {
-INSTANCE: PublishedState
+publish(article) ❌ throws Exception
+archive(article) ✅
+unarchive(article) ❌ throws Exception
+canDelete() ✅ true
}
class ArchivedState {
-INSTANCE: ArchivedState
+publish(article) ❌ throws Exception
+archive(article) ❌ throws Exception
+unarchive(article) ✅
+canDelete() ✅ true
}
class ArticleStateFactory {
+getState(article) ArticleState
+getState(status) ArticleState
}
ArticleState <|.. DraftState
ArticleState <|.. PublishedState
ArticleState <|.. ArchivedState
ArticleStateFactory ..> ArticleState
实现代码:
// 1. 状态接口
public interface ArticleState {
void publish(ArticleEntity article);
void archive(ArticleEntity article);
void unarchive(ArticleEntity article);
boolean canDelete();
}
// 2. 草稿状态
@Slf4j
public class DraftState implements ArticleState {
private static final DraftState INSTANCE = new DraftState();
public static DraftState getInstance() {
return INSTANCE;
}
@Override
public void publish(ArticleEntity article) {
log.info("草稿 → 已发布: articleId={}", article.getId());
article.setStatus(ArticleStatus.PUBLISHED.getCode());
article.setPublishTime(LocalDateTime.now());
}
@Override
public void archive(ArticleEntity article) {
throw new BusinessException(SystemErrorCode.INVALID_OPERATION,
"草稿不能归档");
}
@Override
public void unarchive(ArticleEntity article) {
throw new BusinessException(SystemErrorCode.INVALID_OPERATION,
"草稿不需要恢复");
}
@Override
public boolean canDelete() {
return true;
}
}
// 3. 已发布状态
@Slf4j
public class PublishedState implements ArticleState {
private static final PublishedState INSTANCE = new PublishedState();
public static PublishedState getInstance() {
return INSTANCE;
}
@Override
public void publish(ArticleEntity article) {
throw new BusinessException(SystemErrorCode.INVALID_OPERATION,
"文章已发布,不能重复发布");
}
@Override
public void archive(ArticleEntity article) {
log.info("已发布 → 已归档: articleId={}", article.getId());
article.setStatus(ArticleStatus.ARCHIVED.getCode());
}
@Override
public void unarchive(ArticleEntity article) {
throw new BusinessException(SystemErrorCode.INVALID_OPERATION,
"已发布文章不需要恢复");
}
@Override
public boolean canDelete() {
return true;
}
}
// 4. 状态工厂(单例模式)
@Component
public class ArticleStateFactory {
public ArticleState getState(ArticleEntity article) {
return getState(article.getStatus());
}
public ArticleState getState(Integer statusCode) {
ArticleStatus status = ArticleStatus.fromCode(statusCode);
return switch (status) {
case DRAFT -> DraftState.getInstance();
case PUBLISHED -> PublishedState.getInstance();
case ARCHIVED -> ArchivedState.getInstance();
};
}
}
使用方式 (简洁清晰):
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl {
private final ArticleMapper articleMapper;
private final ArticleStateFactory stateFactory;
private final ApplicationEventPublisher eventPublisher;
public void publishArticle(Long id) {
// 1. 加载实体
ArticleEntity article = articleMapper.selectById(id);
if (article == null) {
throw new BusinessException("文章不存在");
}
// 2. 获取当前状态
ArticleState currentState = stateFactory.getState(article);
// 3. 委托状态对象处理(状态内部会验证是否允许)
currentState.publish(article);
// 4. 保存
articleMapper.updateById(article);
// 5. 发布事件
eventPublisher.publishEvent(new ArticlePublishedEvent(
this, id, article.getAuthorId(), article.getTitle()
));
}
}
优势总结:
| 对比维度 | 传统if-else | State Pattern |
|---|---|---|
| 代码行数 | ~60行 | ~30行 (Service) + 状态类 |
| 新增状态 | 修改所有方法 | 只新增一个状态类 ✅ |
| 状态规则 | 分散在各方法 | 集中在状态类 ✅ |
| 可测试性 | 难以单测 | 易于单测每个状态 ✅ |
| 可读性 | 复杂嵌套 | 清晰直观 ✅ |
2. Chain of Responsibility (责任链模式) ⭐⭐
问题分析
文章发布前需要多步处理内容:
- XSS过滤 - 防止脚本注入攻击
- Markdown解析 - 转换为HTML
- TOC生成 - 提取标题生成目录
- 摘要提取 - 自动生成摘要
如果直接写在Service中:
// ❌ 传统实现(臃肿且难以扩展)
@Override
protected void preSave(ArticleEntity entity) {
String content = entity.getContent();
// 1. XSS过滤
content = content.replace("<script>", "");
content = content.replace("</script>", "");
// ... 更多过滤逻辑
// 2. Markdown解析
String html = markdownToHtml(content); // 复杂逻辑
entity.setContentHtml(html);
// 3. TOC生成
List<Heading> headings = extractHeadings(html);
String tocJson = JsonUtils.toJson(headings);
entity.setTocJson(tocJson);
// 4. 摘要提取
String summary = html.replaceAll("<[^>]*>", "")
.substring(0, Math.min(200, html.length()));
entity.setSummary(summary);
// ❌ 代码混在一起,难以测试和扩展
}
问题:
- ❌ 逻辑混杂(Service承担了太多职责)
- ❌ 难以测试(无法单独测试某个处理步骤)
- ❌ 难以扩展(新增处理步骤需要修改Service)
- ❌ 顺序固定(无法灵活调整处理顺序)
Chain of Responsibility 解决方案
核心思想: 每个处理器只负责一件事,通过链式调用组装成流水线。
类图设计:
classDiagram
class ProcessResult {
+String originalContent
+String title
+String html
+String tocJson
+String summary
+boolean success
}
class ContentProcessor {
<<interface>>
+process(result) ProcessResult
+setNext(processor) void
}
class AbstractContentProcessor {
<<abstract>>
-ContentProcessor next
+process(result) ProcessResult
#doProcess(result) ProcessResult
+setNext(processor) void
}
class XssFilterProcessor {
+doProcess(result) ProcessResult
}
class MarkdownParserProcessor {
+doProcess(result) ProcessResult
}
class TocGeneratorProcessor {
+doProcess(result) ProcessResult
}
class SummaryExtractorProcessor {
+doProcess(result) ProcessResult
}
ContentProcessor <|.. AbstractContentProcessor
AbstractContentProcessor <|-- XssFilterProcessor
AbstractContentProcessor <|-- MarkdownParserProcessor
AbstractContentProcessor <|-- TocGeneratorProcessor
AbstractContentProcessor <|-- SummaryExtractorProcessor
AbstractContentProcessor o-- ContentProcessor: next
实现代码:
// 1. 处理结果类
@Data
@Accessors(chain = true)
public class ProcessResult {
private String originalContent;
private String title;
private String html;
private String tocJson;
private String summary;
private boolean success = true;
}
// 2. 处理器接口
public interface ContentProcessor {
ProcessResult process(ProcessResult input);
void setNext(ContentProcessor next);
}
// 3. 抽象处理器(模板方法)
public abstract class AbstractContentProcessor implements ContentProcessor {
protected ContentProcessor next;
@Override
public final ProcessResult process(ProcessResult input) {
// 处理当前步骤
ProcessResult result = doProcess(input);
// 传递给下一个处理器
if (next != null) {
return next.process(result);
}
return result;
}
protected abstract ProcessResult doProcess(ProcessResult input);
@Override
public void setNext(ContentProcessor next) {
this.next = next;
}
}
// 4. 具体处理器示例
@Component
@Slf4j
public class MarkdownParserProcessor extends AbstractContentProcessor {
@Override
protected ProcessResult doProcess(ProcessResult input) {
log.debug("开始Markdown解析");
String content = input.getOriginalContent();
// 简化版Markdown解析(实际应使用Flexmark/CommonMark)
String html = content
.replaceAll("^# (.+)$", "<h1>$1</h1>")
.replaceAll("^## (.+)$", "<h2>$1</h2>")
.replaceAll("^### (.+)$", "<h3>$1</h3>")
.replaceAll("^\\*\\*(.+?)\\*\\*", "<strong>$1</strong>")
.replaceAll("^\\*(.+?)\\*", "<em>$1</em>");
return input.setHtml(html);
}
}
// 5. TOC生成器
@Component
@Slf4j
public class TocGeneratorProcessor extends AbstractContentProcessor {
@Override
protected ProcessResult doProcess(ProcessResult input) {
log.debug("开始生成TOC");
String html = input.getHtml();
List<TocItem> toc = extractHeadings(html);
String tocJson = JsonUtils.toJson(toc);
return input.setTocJson(tocJson);
}
private List<TocItem> extractHeadings(String html) {
// 提取 <h1> <h2> <h3> 标签
// 实际实现会更复杂
return new ArrayList<>();
}
}
配置链式组装:
@Configuration
public class ContentProcessorChainConfig {
@Bean
public ContentProcessor contentProcessorChain(
XssFilterProcessor xssFilter,
MarkdownParserProcessor markdownParser,
TocGeneratorProcessor tocGenerator,
SummaryExtractorProcessor summaryExtractor
) {
// 组装处理链:XSS → Markdown → TOC → Summary
xssFilter.setNext(markdownParser);
markdownParser.setNext(tocGenerator);
tocGenerator.setNext(summaryExtractor);
return xssFilter; // 返回链头
}
}
使用方式 (简洁优雅):
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl {
private final ContentProcessor contentProcessorChain;
@Override
protected void preSave(ArticleEntity entity) {
// 一行代码完成所有处理!
ProcessResult result = contentProcessorChain.process(
new ProcessResult()
.setOriginalContent(entity.getContent())
.setTitle(entity.getTitle())
);
// 设置处理结果
entity.setContentHtml(result.getHtml());
entity.setTocJson(result.getTocJson());
entity.setSummary(result.getSummary());
}
}
优势总结:
| 特性 | 优势 |
|---|---|
| 单一职责 | 每个Processor只负责一件事 ✅ |
| 易于测试 | 可单独测试每个Processor ✅ |
| 灵活组装 | 顺序可配置化 ✅ |
| 易于扩展 | 新增Processor不影响现有代码 ✅ |
3. Observer Pattern (Spring Event) ⭐⭐⭐
问题分析
文章发布后需要触发多个副作用:
- 清理缓存 - 清除相关文章列表缓存
- 初始化统计 - 创建统计记录(浏览/点赞/评论数)
- 生成向量 - 调用Embedding API生成向量
- 发送通知 - 通知关注者
如果直接写在Service中:
// ❌ 传统实现(耦合严重)
public void publishArticle(Long id) {
// 1. 主流程
ArticleEntity article = articleMapper.selectById(id);
articleState.publish(article);
articleMapper.updateById(article);
// 2. 副作用(❌ 耦合在一起)
try {
cacheService.clearArticleListCache();
statsService.initStats(id);
embeddingService.generateEmbedding(id); // 可能很慢!
notificationService.notifyFollowers(id);
} catch (Exception e) {
log.error("副作用处理失败", e);
// ❌ 副作用失败会影响主流程吗?需要回滚吗?
}
}
问题:
- ❌ 主流程与副作用耦合
- ❌ 副作用阻塞主流程(Embedding API调用很慢)
- ❌ 难以扩展(新增副作用需要修改Service)
- ❌ 事务边界不清晰
Spring Event 解决方案
核心思想: 使用发布-订阅模式,主流程只负责发布事件,监听器异步处理副作用。
时序图:
sequenceDiagram
participant Controller
participant Service as ArticleServiceImpl
participant Publisher as ApplicationEventPublisher
participant DB as Database
participant Listener as ArticleEventListener
participant Embedding as EmbeddingService
Controller->>Service: publishArticle(id)
Service->>Service: 获取状态并验证
Service->>Service: 委托State处理
Service->>DB: 更新状态到数据库
Note over Service,Publisher: 主流程完成,发布事件
Service->>Publisher: publishEvent(ArticlePublishedEvent)
Service-->>Controller: 返回成功
Note over Publisher,Listener: 异步执行(不阻塞主流程)
Publisher->>Listener: @EventListener (异步)
par 并行处理多个副作用
Listener->>Listener: 清理缓存
Listener->>Listener: 初始化统计
Listener->>Embedding: 生成向量(异步)
Listener->>Listener: 发送通知
end
实现代码:
// 1. 定义事件
public class ArticlePublishedEvent extends ApplicationEvent {
private final Long articleId;
private final Long authorId;
private final String title;
public ArticlePublishedEvent(Object source, Long articleId,
Long authorId, String title) {
super(source);
this.articleId = articleId;
this.authorId = authorId;
this.title = title;
}
// Getters...
}
// 2. Service发布事件
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl {
private final ApplicationEventPublisher eventPublisher;
public void publishArticle(Long id) {
// 主流程(同步事务)
ArticleEntity article = articleMapper.selectById(id);
articleState.publish(article);
articleMapper.updateById(article);
// 发布事件(不阻塞)
eventPublisher.publishEvent(new ArticlePublishedEvent(
this,
article.getId(),
article.getAuthorId(),
article.getTitle()
));
// ✅ 主流程立即返回,不等待副作用
}
}
// 3. 事件监听器
@Component
@RequiredArgsConstructor
@Slf4j
public class ArticleEventListener {
private final EmbeddingService embeddingService;
// TODO: 注入其他服务(缓存、统计等)
/**
* 处理文章发布事件
*
* @Async 使监听器在单独线程池执行
* @EventListener 声明为事件监听器
*/
@Async
@EventListener
public void handleArticlePublished(ArticlePublishedEvent event) {
log.info("处理文章发布事件: articleId={}, title={}",
event.getArticleId(), event.getTitle());
try {
// 1. 清理缓存
clearCache(event.getArticleId());
// 2. 初始化统计
initStats(event.getArticleId());
// 3. 生成向量(异步,不影响其他步骤)
generateEmbedding(event);
// 4. 发送通知
sendNotification(event);
} catch (Exception e) {
log.error("副作用处理失败,不影响主流程", e);
// ✅ 副作用失败不影响主流程(文章已经成功发布)
}
}
private void generateEmbedding(ArticlePublishedEvent event) {
embeddingService.generateAndSaveAsync(event.getArticleId());
}
}
异步配置:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("article-event-");
executor.initialize();
return executor;
}
}
优势总结:
| 特性 | 优势 |
|---|---|
| 解耦 | 主流程与副作用完全分离 ✅ |
| 异步 | 副作用不阻塞主流程 ✅ |
| 可扩展 | 新增监听器无需修改Service ✅ |
| 事务清晰 | 主流程与副作用事务边界分明 ✅ |
| 容错性 | 副作用失败不影响主流程 ✅ |
🎯 向量搜索架构
核心组件
graph TB
subgraph "Embedding层"
Interface[EmbeddingService Interface]
Mock[MockEmbeddingServiceImpl]
OpenAI[OpenAIEmbeddingServiceImpl<Future>]
DashScope[DashScopeEmbeddingServiceImpl<Future>]
end
subgraph "搜索层"
VectorSearch[VectorSearchService]
Mapper[ArticleMapper]
end
subgraph "数据层"
DB[(MySQL 9.4 VECTOR)]
end
Interface -.实现.-> Mock
Interface -.实现.-> OpenAI
Interface -.实现.-> DashScope
VectorSearch --> Interface
VectorSearch --> Mapper
Mapper --> DB
style Mock fill:#90EE90
style OpenAI fill:#e8f5e9
style DB fill:#ffe8e8
三级降级策略
flowchart TB
A[findRelatedArticles] --> B{文章有向量?}
B -->|✅ 有| C[Level 1: VECTOR相似度搜索]
C --> D{查询成功?}
D -->|✅| E[返回结果]
D -->|❌| F[Level 2: 同分类文章]
B -->|❌ 无| F
F --> G{查询成功?}
G -->|✅| E
G -->|❌| H[Level 3: 最新文章]
H --> E
style C fill:#90EE90
style F fill:#fff4e1
style H fill:#ffe8e8
MySQL COSINE_DISTANCE 查询
-- 核心SQL(在ArticleMapper中)
SELECT
id,
title,
summary,
COSINE_DISTANCE(embedding, :queryVector) as similarity
FROM art_article
WHERE is_deleted = 0
AND status = 2 -- 只查已发布
AND id != :excludeId -- 排除当前文章
AND embedding IS NOT NULL -- 必须有向量
ORDER BY similarity ASC -- 距离越小越相似
LIMIT :limit;
-- COSINE_DISTANCE值域: [0, 2]
-- 0 = 完全相同
-- 1 = 正交(无关)
-- 2 = 完全相反
📊 性能优化
1. 乐观锁防止并发冲突
@TableName("art_article")
public class ArticleEntity {
@Version // MyBatis-Plus乐观锁
private Integer version;
}
2. 逻辑删除减少数据迁移
@TableLogic // MyBatis-Plus逻辑删除
private Integer isDeleted; // 0=正常 1=已删除
3. 索引优化
-- 复合索引(状态 + 发布时间)
CREATE INDEX idx_status_publish_time
ON art_article(status, publish_time DESC);
-- 作者索引
CREATE INDEX idx_author_id
ON art_article(author_id);
-- 分类索引
CREATE INDEX idx_category_id
ON art_article(category_id);
-- 未来:向量索引(MySQL 9.5+支持HNSW)
-- CREATE INDEX idx_embedding
-- ON art_article(embedding) USING HNSW;
🛠️ 技术决策
为什么选择 State Pattern?
| 决策因素 | 说明 |
|---|---|
| 复杂度降低 | 消除30+行if-else嵌套 |
| 可维护性 | 状态规则集中管理 |
| 可测试性 | 每个状态可单独单测 |
| 扩展性 | 新增状态只需新增类 |
为什么选择 MySQL VECTOR 而非 Elasticsearch?
| 对比项 | MySQL VECTOR | Elasticsearch |
|---|---|---|
| 部署 | ⭐ (无额外组件) | ⭐⭐⭐ (需集群) |
| 运维 | ⭐ (低) | ⭐⭐⭐ (高) |
| 学习曲线 | ⭐ (SQL) | ⭐⭐⭐ (DSL) |
| 事务 | ✅ ACID | ❌ 最终一致 |
| 向量搜索 | ✅ (9.4+) | ✅ |
| 性能 | 中等 | 高 |
结论: 对于博客规模(<10万文章),MySQL VECTOR性价比更高。
为什么使用 MapStruct 而非手写?
| 对比项 | MapStruct | 手写 |
|---|---|---|
| 性能 | ✅ 编译期生成 | ✅ 同样 |
| 安全性 | ✅ 编译期检查 | ❌ 运行时才发现错误 |
| 维护性 | ✅ 自动同步字段变化 | ❌ 手动维护 |
| 代码量 | ⭐ (注解) | ⭐⭐⭐ (大量setter/getter) |
📚 延伸阅读
- API完整文档 - 所有接口详细说明