文章模块 (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)支持 |
| ORM | MyBatis-Plus 3.5.14 | 强大的CRUD能力 |
| Bean映射 | MapStruct | 编译期生成,零性能损耗 |
| 设计模式 | State + Chain + Observer | 高内聚低耦合 |
| Embedding | Mock (可切换真实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
推荐学习顺序
🚀 快速开始
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"
发生的事情:
- ✅ 状态变更:DRAFT → PUBLISHED
- ✅ 设置发布时间
- ✅ 触发
ArticlePublishedEvent - ✅ 异步生成向量(MockEmbedding)
- ✅ 初始化统计数据
- ✅ 清理相关缓存
第三步:获取相关推荐
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 VECTOR | Elasticsearch |
|---|---|---|
| 部署复杂度 | ⭐ (无额外组件) | ⭐⭐⭐ (需要集群) |
| 事务支持 | ✅ 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有以下优势:
- 零额外组件 - 不需要部署ES集群
- ACID保证 - 事务一致性强
- 运维简单 - 无需维护额外组件
- 学习成本低 - 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: 我们实现了三级降级策略:
- Level 1 - VECTOR相似度搜索(最优)
- Level 2 - 同分类文章(次优)
- Level 3 - 最新文章(保底)
确保推荐永不失败!
Q5: COSINE_DISTANCE值域是什么?
A: [0, 2]
0= 完全相同1= 正交(无关)2= 完全相反
查询时按 ASC 排序,距离越小越相似。
🛠️ 相关资源
配置文档
- MySQL 9.4 VECTOR: https://dev.mysql.com/doc/refman/9.0/en/vector.html
- OpenAI Embeddings: https://platform.openai.com/docs/guides/embeddings
API文档
- 完整API参考
- Swagger UI: http://localhost:8080/swagger-ui.html
开发工具
- 测试脚本:
blog-article-service/src/test/ - Flyway脚本:
blog-application/src/main/resources/db/