跳到主要内容

架构设计 (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-elseState Pattern
代码行数~60行~30行 (Service) + 状态类
新增状态修改所有方法只新增一个状态类 ✅
状态规则分散在各方法集中在状态类 ✅
可测试性难以单测易于单测每个状态 ✅
可读性复杂嵌套清晰直观 ✅

2. Chain of Responsibility (责任链模式) ⭐⭐

问题分析

文章发布前需要多步处理内容:

  1. XSS过滤 - 防止脚本注入攻击
  2. Markdown解析 - 转换为HTML
  3. TOC生成 - 提取标题生成目录
  4. 摘要提取 - 自动生成摘要

如果直接写在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) ⭐⭐⭐

问题分析

文章发布后需要触发多个副作用:

  1. 清理缓存 - 清除相关文章列表缓存
  2. 初始化统计 - 创建统计记录(浏览/点赞/评论数)
  3. 生成向量 - 调用Embedding API生成向量
  4. 发送通知 - 通知关注者

如果直接写在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 VECTORElasticsearch
部署⭐ (无额外组件)⭐⭐⭐ (需集群)
运维⭐ (低)⭐⭐⭐ (高)
学习曲线⭐ (SQL)⭐⭐⭐ (DSL)
事务✅ ACID❌ 最终一致
向量搜索✅ (9.4+)
性能中等

结论: 对于博客规模(<10万文章),MySQL VECTOR性价比更高。

为什么使用 MapStruct 而非手写?

对比项MapStruct手写
性能✅ 编译期生成✅ 同样
安全性✅ 编译期检查❌ 运行时才发现错误
维护性✅ 自动同步字段变化❌ 手动维护
代码量⭐ (注解)⭐⭐⭐ (大量setter/getter)

📚 延伸阅读