跳到主要内容

文章模块 (Article Module)

📖 概述

文章模块是博客系统的核心业务模块,实现了功能完整的内容管理系统(CMS),并集成了MySQL 9.4 原生向量搜索实现智能文章推荐。

核心特性

  • MySQL VECTOR搜索 - 无需Elasticsearch,利用MySQL 9.4原生向量类型实现语义搜索
  • 状态模式重构 - 消除复杂if-else,使用State Pattern管理文章生命周期
  • 责任链处理 - 灵活的内容处理管道(XSS过滤、Markdown解析、TOC生成)
  • Spring Event解耦 - 异步处理副作用(缓存、统计、向量生成、通知)
  • Mock Embedding - 零配置可用,支持平滑切换到OpenAI/DashScope/Ollama
  • 三级降级 - 向量搜索 → 同分类 → 最新文章,确保推荐永不失败

技术选型

组件技术说明
数据库MySQL 9.4原生VECTOR(1536)支持
ORMMyBatis-Plus 3.5.14强大的CRUD能力
Bean映射MapStruct编译期生成,零性能损耗
设计模式State + Chain + Observer高内聚低耦合
EmbeddingMock (可切换真实API)text-embedding-3-small (1536维)

适用场景

  • 📝 博客文章管理与发布
  • 🔍 基于语义的智能文章推荐
  • 📊 文章浏览、点赞、收藏统计
  • 🏷️ 分类与标签管理
  • 💬 评论系统集成(预留)

🎯 学习路线图

建议按以下顺序学习本模块:

graph LR
A[架构设计] --> B[数据库设计]
B --> C[状态模式]
C --> D[责任链模式]
D --> E[向量搜索]
E --> F[API集成]

style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#f0f0f0
style D fill:#e8f5e9
style E fill:#ffe8f0
style F fill:#e8f5ff

推荐学习顺序

  1. 架构设计 - 理解整体架构和三大设计模式
  2. API参考 - 完整接口文档

🚀 快速开始

5分钟创建并发布文章

第一步:创建草稿

curl -X POST http://localhost:8080/api/v1/admin/articles \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"title": "我的第一篇文章",
"content": "# Hello World\n\n这是我的第一篇博客文章。",
"summary": "博客首秀",
"categoryId": 1,
"coverImage": "https://example.com/cover.jpg"
}'

响应:

{
"code": 200,
"data": "1234567890123456789",
"message": "创建成功"
}

第二步:发布文章

curl -X PUT http://localhost:8080/api/v1/admin/articles/1234567890123456789/publish \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

发生的事情:

  1. ✅ 状态变更:DRAFT → PUBLISHED
  2. ✅ 设置发布时间
  3. ✅ 触发 ArticlePublishedEvent
  4. ✅ 异步生成向量(MockEmbedding)
  5. ✅ 初始化统计数据
  6. ✅ 清理相关缓存

第三步:获取相关推荐

curl http://localhost:8080/api/v1/articles/1234567890123456789/related?limit=5

响应 (基于向量相似度):

{
"code": 200,
"data": [
{
"id": "9876543210987654321",
"title": "相关文章标题",
"summary": "基于语义相似度推荐",
"publishTime": "2024-01-01T10:00:00"
}
]
}

💡 核心概念

1. MySQL VECTOR 向量搜索

什么是VECTOR类型?

MySQL 9.4引入的新数据类型,存储多维浮点数向量,用于AI/ML场景的相似度搜索。

我们如何使用?

-- 表结构
CREATE TABLE art_article (
id BIGINT PRIMARY KEY,
title VARCHAR(200),
embedding VECTOR(1536), -- OpenAI text-embedding-3-small维度
...
);

-- 相似度查询 (COSINE_DISTANCE)
SELECT id, title,
COSINE_DISTANCE(embedding, :queryVector) as similarity
FROM art_article
WHERE is_deleted = 0 AND status = 2
ORDER BY similarity ASC -- 距离越小越相似
LIMIT 5;

优势对比:

特性MySQL VECTORElasticsearch
部署复杂度⭐ (无额外组件)⭐⭐⭐ (需要集群)
事务支持✅ ACID保证❌ 最终一致性
运维成本
数据一致性强一致最终一致
学习曲线平缓陡峭

2. State Pattern (状态模式)

问题:文章有多种状态(草稿、已发布、已归档),不同状态下允许的操作不同,直接用if-else会产生复杂的分支逻辑。

解决方案:将每个状态封装成独立的类,状态转换由状态对象自己处理。

状态流转图:

stateDiagram-v2
[*] --> Draft: 创建文章
Draft --> Published: publish()
Draft --> [*]: delete()
Published --> Archived: archive()
Published --> [*]: delete()
Archived --> Published: unarchive()
Archived --> [*]: delete()

note right of Draft
只能发布或删除
不能归档
end note

note right of Published
可归档或删除
不能再发布
end note

note right of Archived
可恢复或删除
不能归档
end note

代码示例:

// 状态接口
public interface ArticleState {
void publish(ArticleEntity article);
void archive(ArticleEntity article);
void unarchive(ArticleEntity article);
boolean canDelete();
}

// 草稿状态
public class DraftState implements ArticleState {
@Override
public void publish(ArticleEntity article) {
article.setStatus(ArticleStatus.PUBLISHED.getCode());
article.setPublishTime(LocalDateTime.now());
// ✅ 允许发布
}

@Override
public void archive(ArticleEntity article) {
throw new BusinessException("草稿不能归档");
// ❌ 不允许归档
}
}

优势:

  • ✅ 消除30+行if-else逻辑
  • ✅ 符合开闭原则(新增状态只需新增类)
  • ✅ 单一职责(每个状态类只负责自己的转换)

3. Chain of Responsibility (责任链模式)

问题:文章发布前需要多步处理(XSS过滤、Markdown转HTML、生成目录、提取摘要),如果写在一起代码会很臃肿。

解决方案:构建处理器链,每个处理器只负责一件事,可灵活组装。

处理流程:

flowchart LR
A[原始Markdown] --> B[XssFilterProcessor]
B --> C[MarkdownParserProcessor]
C --> D[TocGeneratorProcessor]
D --> E[SummaryExtractorProcessor]
E --> F[ProcessResult]

style B fill:#ffe8e8
style C fill:#e8f5e9
style D fill:#e1f5ff
style E fill:#fff4e1

代码示例:

@Configuration
public class ContentProcessorChainConfig {

@Bean
public ContentProcessor contentProcessorChain(
XssFilterProcessor xssFilter,
MarkdownParserProcessor markdownParser,
TocGeneratorProcessor tocGenerator,
SummaryExtractorProcessor summaryExtractor
) {
// 组装处理链
xssFilter.setNext(markdownParser);
markdownParser.setNext(tocGenerator);
tocGenerator.setNext(summaryExtractor);

return xssFilter; // 返回链头
}
}

// 使用
ProcessResult result = contentProcessorChain.process(new ProcessResult()
.setOriginalContent(article.getContent())
.setTitle(article.getTitle())
);

article.setContentHtml(result.getHtml());
article.setTocJson(result.getTocJson());
article.setSummary(result.getSummary());

优势:

  • ✅ 单一职责(每个Processor独立)
  • ✅ 易于测试(可单独测试每个Processor)
  • ✅ 灵活组装(顺序可配置)
  • ✅ 易于扩展(新增Processor无需改动现有代码)

4. Observer Pattern (Spring Event)

问题:文章发布后需要触发多个副作用(清缓存、初始化统计、生成向量、发通知),写在一起会导致耦合。

解决方案:使用Spring的ApplicationEvent机制,发布事件后由监听器异步处理。

事件流转:

sequenceDiagram
participant Service as ArticleServiceImpl
participant Publisher as ApplicationEventPublisher
participant Listener as ArticleEventListener
participant Cache as CacheService
participant Stats as StatsService
participant Embedding as EmbeddingService

Service->>Publisher: publishEvent(ArticlePublishedEvent)
Service-->>Service: 继续执行主流程

Publisher->>Listener: @EventListener (异步)

par 并行处理副作用
Listener->>Cache: 清理缓存
Listener->>Stats: 初始化统计
Listener->>Embedding: 生成向量(异步)
end

代码示例:

// 定义事件
public class ArticlePublishedEvent extends ApplicationEvent {
private final Long articleId;
private final Long authorId;
private final String title;
}

// 发布事件
@Service
public class ArticleServiceImpl {
private final ApplicationEventPublisher eventPublisher;

public void publishArticle(Long id) {
// 1. 更新状态(主流程)
ArticleEntity article = articleMapper.selectById(id);
articleState.publish(article);
articleMapper.updateById(article);

// 2. 发布事件(副作用解耦)
eventPublisher.publishEvent(new ArticlePublishedEvent(
this, id, article.getAuthorId(), article.getTitle()
));
}
}

// 监听事件
@Component
@RequiredArgsConstructor
public class ArticleEventListener {

private final EmbeddingService embeddingService;

@Async // 异步执行
@EventListener
public void handleArticlePublished(ArticlePublishedEvent event) {
// 1. 清理缓存
clearCache(event.getArticleId());

// 2. 初始化统计
initStats(event.getArticleId());

// 3. 生成向量
embeddingService.generateAndSaveAsync(event.getArticleId());

// 4. 发送通知
sendNotification(event);
}
}

优势:

  • ✅ 解耦主流程与副作用
  • ✅ 异步执行不阻塞主流程
  • ✅ 易于扩展(新增监听器无需修改代码)
  • ✅ 符合单一职责原则

📊 数据流向

完整发布流程

graph TB
subgraph 客户端
A[管理员点击发布] --> B[POST /admin/articles/:id/publish]
end

subgraph Controller
B --> C[参数校验]
C --> D[调用Service]
end

subgraph Service
D --> E{State Pattern}
E --> F[DraftState.publish]
F --> G[更新状态+设置发布时间]
G --> H[保存到数据库]
H --> I[发布Event]
end

subgraph EventListener异步
I --> J[清理缓存]
I --> K[初始化统计]
I --> L[生成向量]
I --> M[发送通知]
end

subgraph 向量生成
L --> N{是否配置真实API?}
N -->|是| O[调用OpenAI API]
N -->|否| P[MockEmbedding生成]
O --> Q[保存到embedding字段]
P --> Q
end

style E fill:#ffe8e8
style I fill:#90EE90
style N fill:#e1f5ff

🎓 进阶学习

深入理解


❓ 常见问题

Q1: 为什么使用MySQL VECTOR而不是Elasticsearch?

A: MySQL VECTOR有以下优势:

  1. 零额外组件 - 不需要部署ES集群
  2. ACID保证 - 事务一致性强
  3. 运维简单 - 无需维护额外组件
  4. 学习成本低 - SQL语法,无需学习ES DSL

唯一劣势是向量搜索性能略低于ES,但对于博客规模(<10万文章)完全够用。

Q2: MockEmbedding的向量质量如何?

A: MockEmbedding使用以下策略保证基本可用:

  • 确定性 - 相同文本生成相同向量(使用hashCode作seed)
  • 归一化 - 向量模长为1,适合余弦相似度
  • 维度一致 - 1536维,与OpenAI兼容

虽然语义质量不如真实API,但可用于开发测试和Demo。生产环境建议切换到真实API(3步配置即可)。

Q3: 如何切换到OpenAI Embedding?

A: 非常简单:

# application.yml
embedding:
provider: openai # 从 mock 改为 openai
openai:
api-key: ${OPENAI_API_KEY}
model: text-embedding-3-small

无需修改任何业务代码!

Q4: 向量搜索失败怎么办?

A: 我们实现了三级降级策略:

  1. Level 1 - VECTOR相似度搜索(最优)
  2. Level 2 - 同分类文章(次优)
  3. Level 3 - 最新文章(保底)

确保推荐永不失败!

Q5: COSINE_DISTANCE值域是什么?

A: [0, 2]

  • 0 = 完全相同
  • 1 = 正交(无关)
  • 2 = 完全相反

查询时按 ASC 排序,距离越小越相似。


🛠️ 相关资源

配置文档

API文档

开发工具

  • 测试脚本: blog-article-service/src/test/
  • Flyway脚本: blog-application/src/main/resources/db/

📝 下一步