feat(docs): add interactive demo components for technical appendices

Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
sanbuphy
2026-02-06 03:34:50 +08:00
parent e8bba6f7c0
commit 7c70c37072
171 changed files with 69830 additions and 6689 deletions
@@ -0,0 +1,534 @@
<template>
<div class="clean-architecture-demo">
<div class="demo-header">
<h4>🏗 整洁架构Clean Architecture与分层架构</h4>
<p class="subtitle">分层架构是整洁架构的基础理解两者的关系有助于构建更灵活的系统</p>
</div>
<!-- 架构对比 -->
<div class="architecture-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="comparison-content">
<!-- 传统分层架构 -->
<div v-if="currentTab === 'layered'" class="tab-panel">
<div class="arch-diagram layered">
<div class="layer-box controller">
<div class="layer-title">Controller </div>
<div class="layer-desc">接收请求参数校验</div>
</div>
<div class="arrow down"> 依赖</div>
<div class="layer-box service">
<div class="layer-title">Service </div>
<div class="layer-desc">业务逻辑事务管理</div>
</div>
<div class="arrow down"> 依赖</div>
<div class="layer-box repository">
<div class="layer-title">Repository </div>
<div class="layer-desc">数据访问ORM 映射</div>
</div>
<div class="arrow down"> 依赖</div>
<div class="layer-box domain">
<div class="layer-title">Domain </div>
<div class="layer-desc">实体定义业务规则</div>
</div>
</div>
<div class="arch-characteristics">
<h5>📌 传统分层架构特点</h5>
<ul>
<li><strong>垂直依赖</strong>上层直接依赖下层依赖方向从上到下</li>
<li><strong>简单直观</strong>结构清晰易于理解和实现</li>
<li><strong>适合中小型项目</strong>快速开发上手简单</li>
<li><strong>潜在问题</strong>底层变更可能影响上层循环依赖风险</li>
</ul>
</div>
</div>
<!-- 整洁架构 -->
<div v-else-if="currentTab === 'clean'" class="tab-panel">
<div class="arch-diagram clean">
<div class="clean-layers">
<div class="clean-layer framework">
<div class="layer-name">框架与驱动层</div>
<div class="layer-items">Web / DB / UI / 外部接口</div>
</div>
<div class="clean-layer interface">
<div class="layer-name">接口适配层</div>
<div class="layer-items">Controller / Gateway / Presenter</div>
</div>
<div class="clean-layer application">
<div class="layer-name">应用层</div>
<div class="layer-items">Service / UseCase / DTO</div>
</div>
<div class="clean-layer domain">
<div class="layer-name">领域层核心</div>
<div class="layer-items">Entity / ValueObject / DomainService</div>
</div>
</div>
<div class="dependency-rule">
<div class="rule-arrow">
<span class="arrow-line"></span>
<span class="arrow-head"> 依赖方向</span>
</div>
<div class="rule-text">
外层依赖内层内层不依赖外层
</div>
</div>
</div>
<div class="arch-characteristics">
<h5>📌 整洁架构特点</h5>
<ul>
<li><strong>依赖倒置</strong>依赖方向从外到内通过接口隔离</li>
<li><strong>领域为核心</strong>业务逻辑位于中心独立于框架</li>
<li><strong>可测试性强</strong>核心业务可脱离框架进行单元测试</li>
<li><strong>技术无关</strong>可轻松切换数据库框架等外部技术</li>
</ul>
</div>
</div>
<!-- 对比总结 -->
<div v-else class="tab-panel">
<div class="comparison-table">
<table>
<thead>
<tr>
<th>特性</th>
<th>传统分层架构</th>
<th>整洁架构</th>
</tr>
</thead>
<tbody>
<tr>
<td>依赖方向</td>
<td>从上到下</td>
<td>从外到内</td>
</tr>
<tr>
<td>核心业务位置</td>
<td>Service </td>
<td>Domain 中心</td>
</tr>
<tr>
<td>框架依赖</td>
<td>较深 Spring</td>
<td>较浅通过接口隔离</td>
</tr>
<tr>
<td>可测试性</td>
<td>需要集成测试</td>
<td>核心可单元测试</td>
</tr>
<tr>
<td>学习曲线</td>
<td>平缓</td>
<td>较陡</td>
</tr>
<tr>
<td>适用场景</td>
<td>中小型项目快速迭代</td>
<td>大型复杂业务长期维护</td>
</tr>
</tbody>
</table>
</div>
<div class="recommendation">
<h5>💡 选型建议</h5>
<div class="rec-grid">
<div class="rec-card">
<div class="rec-title">选择传统分层架构当...</div>
<ul>
<li>项目规模较小业务相对简单</li>
<li>团队对 DDD 不熟悉</li>
<li>需要快速上线验证市场</li>
<li>技术栈相对固定</li>
</ul>
</div>
<div class="rec-card recommended">
<div class="rec-title">选择整洁架构当...</div>
<ul>
<li>业务复杂领域模型丰富</li>
<li>需要长期维护和演进</li>
<li>需要频繁切换技术栈</li>
<li>团队有较强的设计能力</li>
</ul>
<div class="rec-badge">推荐</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentTab = ref('layered')
const tabs = [
{ id: 'layered', name: '传统分层' },
{ id: 'clean', name: '整洁架构' },
{ id: 'comparison', name: '对比总结' }
]
</script>
<style scoped>
.clean-architecture-demo {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.architecture-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Layered Architecture */
.arch-diagram.layered {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.layer-box {
width: 100%;
max-width: 400px;
padding: 16px;
border-radius: 8px;
text-align: center;
border-left: 4px solid;
}
.layer-box.controller {
background: #f0f9ff;
border-left-color: #52c41a;
}
.layer-box.service {
background: #fff7e6;
border-left-color: #fa8c16;
}
.layer-box.repository {
background: #e6f7ff;
border-left-color: #1890ff;
}
.layer-box.domain {
background: #f6ffed;
border-left-color: #73d13d;
}
.layer-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.layer-desc {
font-size: 12px;
color: #666;
}
.arrow {
color: #909399;
font-size: 12px;
text-align: center;
}
.arch-characteristics {
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.arch-characteristics h5 {
margin: 0 0 12px 0;
color: #1a1a2e;
font-size: 14px;
}
.arch-characteristics ul {
margin: 0;
padding-left: 20px;
}
.arch-characteristics li {
margin: 8px 0;
color: #595959;
font-size: 13px;
line-height: 1.5;
}
/* Clean Architecture */
.arch-diagram.clean {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.clean-layers {
display: flex;
flex-direction: column-reverse;
gap: 8px;
}
.clean-layer {
padding: 12px 16px;
border-radius: 8px;
border-left: 4px solid;
}
.clean-layer.framework {
background: #f0f0f0;
border-left-color: #8c8c8c;
}
.clean-layer.interface {
background: #e6f7ff;
border-left-color: #1890ff;
}
.clean-layer.application {
background: #fff7e6;
border-left-color: #fa8c16;
}
.clean-layer.domain {
background: #f6ffed;
border-left-color: #52c41a;
}
.clean-layer .layer-name {
font-weight: 600;
color: #1a1a2e;
font-size: 14px;
margin-bottom: 4px;
}
.clean-layer .layer-items {
font-size: 12px;
color: #666;
}
.dependency-rule {
background: white;
border-radius: 8px;
padding: 16px;
text-align: center;
border: 2px dashed #1890ff;
}
.rule-arrow {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
}
.rule-arrow .arrow-line {
width: 2px;
height: 20px;
background: #1890ff;
}
.rule-arrow .arrow-head {
color: #1890ff;
font-weight: 600;
font-size: 14px;
}
.rule-text {
color: #595959;
font-size: 13px;
}
/* Comparison Table */
.comparison-table {
overflow-x: auto;
}
.comparison-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.comparison-table th,
.comparison-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.comparison-table th {
background: #f5f7fa;
font-weight: 600;
color: #1a1a2e;
}
.comparison-table tr:hover {
background: #fafafa;
}
/* Recommendation */
.recommendation {
margin-top: 24px;
}
.recommendation h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.rec-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.rec-card {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
position: relative;
}
.rec-card.recommended {
background: #f6ffed;
border: 2px solid #52c41a;
}
.rec-badge {
position: absolute;
top: -10px;
right: 16px;
background: #52c41a;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.rec-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
font-size: 14px;
}
.rec-card ul {
margin: 0;
padding-left: 18px;
}
.rec-card li {
margin: 6px 0;
color: #595959;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 768px) {
.rec-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,340 @@
<template>
<div class="controller-layer-demo">
<div class="demo-header">
<h4>🎮 Controller 请求的"接待员"</h4>
<p class="subtitle">点击流程节点查看 Controller 如何接收和处理请求</p>
</div>
<div class="flow-container">
<!-- 请求发起 -->
<div class="flow-step">
<div class="step-icon">🌐</div>
<div class="step-content">
<div class="step-title">客户端发起请求</div>
<div class="step-code">
POST /api/users/register
Content-Type: application/json
{
"username": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}
</div>
</div>
</div>
<div class="arrow-connector"> 请求到达</div>
<!-- Controller 接收 -->
<div
class="flow-step controller-step"
:class="{ active: showDetails === 'controller' }"
@click="toggleDetails('controller')"
>
<div class="step-icon">🎮</div>
<div class="step-content">
<div class="step-title">Controller 接收并解析请求</div>
<div class="step-code">
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping("/register")
public ResponseEntity&lt;UserDTO&gt; register(
@RequestBody @Valid UserRegisterRequest request
) {
// 调用 Service 处理业务
UserDTO user = userService.register(request);
return ResponseEntity.ok(user);
}
}
</div>
</div>
</div>
<div class="arrow-connector"> 参数校验 + 调用</div>
<!-- 校验逻辑 -->
<div
class="flow-step validation-step"
:class="{ active: showDetails === 'validation' }"
@click="toggleDetails('validation')"
>
<div class="step-icon"></div>
<div class="step-content">
<div class="step-title">参数校验Controller 的职责之一</div>
<div class="step-code">
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Size(min = 6, message = "密码至少6位")
private String password;
}
</div>
<div v-if="showDetails === 'validation'" class="detail-panel">
<h5>为什么校验要放在 Controller</h5>
<ul>
<li>🛡 第一道防线尽早拦截非法请求</li>
<li>📦 减轻下游压力Service 层可以假设数据已清洗</li>
<li>🔧 关注点分离Service 专注于业务不处理格式验证</li>
</ul>
</div>
</div>
</div>
<div class="arrow-connector"> 返回结果</div>
<!-- 响应返回 -->
<div class="flow-step">
<div class="step-icon">📤</div>
<div class="step-content">
<div class="step-title">Controller 封装响应返回给客户端</div>
<div class="step-code">
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"message": "注册成功",
"data": {
"id": 10001,
"username": "张三",
"email": "zhangsan@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
}
</div>
</div>
</div>
</div>
<!-- Controller 职责总结 -->
<div class="controller-summary">
<h5>🎯 Controller 的核心职责</h5>
<div class="duty-grid">
<div class="duty-item">
<div class="duty-icon">📡</div>
<div class="duty-title">接收请求</div>
<div class="duty-desc">映射 HTTP 请求到方法</div>
</div>
<div class="duty-item">
<div class="duty-icon"></div>
<div class="duty-title">参数校验</div>
<div class="duty-desc">基础格式和必填校验</div>
</div>
<div class="duty-item">
<div class="duty-icon">🔄</div>
<div class="duty-title">调用 Service</div>
<div class="duty-desc">将请求转发给业务层</div>
</div>
<div class="duty-item">
<div class="duty-icon">📦</div>
<div class="duty-title">封装响应</div>
<div class="duty-desc">统一响应格式返回</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showDetails = ref('')
const toggleDetails = (section) => {
showDetails.value = showDetails.value === section ? '' : section
}
</script>
<style scoped>
.controller-layer-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.flow-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.flow-step {
display: flex;
gap: 16px;
padding: 16px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
}
.flow-step:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.flow-step.active {
border: 2px solid #409eff;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
}
.controller-step {
border-left: 4px solid #67c23a;
}
.validation-step {
border-left: 4px solid #e6a23c;
}
.step-icon {
font-size: 24px;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 600;
color: #303133;
margin-bottom: 8px;
font-size: 14px;
}
.step-code {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
color: #333;
white-space: pre-wrap;
line-height: 1.5;
}
.arrow-connector {
text-align: center;
padding: 8px;
font-size: 12px;
color: #909399;
font-weight: 500;
}
.detail-panel {
margin-top: 12px;
padding: 16px;
background: #f0f7ff;
border-radius: 8px;
border-left: 4px solid #409eff;
}
.detail-panel h5 {
margin: 0 0 12px 0;
color: #1a1a2e;
font-size: 14px;
}
.detail-panel ul {
margin: 0;
padding-left: 20px;
}
.detail-panel li {
margin: 6px 0;
color: #606266;
font-size: 12px;
line-height: 1.6;
}
.controller-summary {
margin-top: 24px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.controller-summary h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.duty-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.duty-item {
text-align: center;
padding: 16px 12px;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s ease;
}
.duty-item:hover {
background: #e6f7ff;
transform: translateY(-2px);
}
.duty-icon {
font-size: 28px;
margin-bottom: 8px;
}
.duty-title {
font-weight: 600;
color: #303133;
font-size: 13px;
margin-bottom: 4px;
}
.duty-desc {
color: #909399;
font-size: 11px;
}
@media (max-width: 768px) {
.duty-grid {
grid-template-columns: repeat(2, 1fr);
}
.flow-step {
flex-direction: column;
gap: 8px;
}
.step-content {
width: 100%;
}
}
</style>
@@ -0,0 +1,319 @@
<template>
<div class="dependency-direction-demo">
<div class="demo-header">
<h4>🔄 依赖方向分层架构的核心规则</h4>
<p class="subtitle">理解依赖方向才能真正掌握分层架构</p>
</div>
<!-- 依赖方向可视化 -->
<div class="direction-visualization">
<div class="arch-diagram">
<!-- 外层 -->
<div class="layer outer">
<div class="layer-label">外层UI / 外部系统</div>
<div class="layer-box">Controller</div>
</div>
<!-- 依赖箭头 -->
<div class="dependency-arrow down">
<span class="arrow-line"></span>
<span class="arrow-head"> 依赖</span>
</div>
<!-- 中层 -->
<div class="layer middle">
<div class="layer-label">中层应用层</div>
<div class="layer-box">Service</div>
</div>
<!-- 依赖箭头 -->
<div class="dependency-arrow down">
<span class="arrow-line"></span>
<span class="arrow-head"> 依赖</span>
</div>
<!-- 内层 -->
<div class="layer inner">
<div class="layer-label">内层领域层</div>
<div class="layer-box">Domain / Repository</div>
</div>
</div>
<!-- 核心原则说明 -->
<div class="principle-box">
<div class="principle-title">🎯 核心原则依赖倒置DIP</div>
<div class="principle-content">
<p><strong>上层模块不应该依赖下层模块的具体实现而应该依赖于抽象</strong></p>
<div class="rule-list">
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>Controller Service 接口</strong>
<div class="rule-desc">Controller 只依赖 Service 的接口不依赖实现类</div>
</div>
</div>
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>Service Repository 接口</strong>
<div class="rule-desc">Service 只依赖 Repository 接口不关心数据怎么存</div>
</div>
</div>
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>所有层依赖 Domain</strong>
<div class="rule-desc">Domain 是核心被所有上层依赖 Domain 不依赖任何层</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 依赖方向示意图 -->
<div class="direction-diagram">
<h5>📊 依赖方向示意图</h5>
<div class="diagram-content">
<pre class="diagram-code">
Controller Layer
UserController
- @Autowired private IUserService userService;
依赖接口不依赖实现
依赖Dependency
Service Layer
UserServiceImpl
- @Autowired private UserRepository repository;
依赖 Repository 接口
依赖
Repository Layer
UserRepository
- extends JpaRepository&lt;User, Long&gt;
依赖
Domain Layer (核心领域)
User (Entity)
- 不包含任何层依赖
- 被所有层依赖
</pre>
</div>
</div>
</div>
</template>
<script setup>
// Component logic can be added here if needed
</script>
<style scoped>
.dependency-direction-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.direction-visualization {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.arch-diagram {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.layer {
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-label {
font-size: 11px;
color: #909399;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.layer-box {
padding: 16px 20px;
background: #f5f7fa;
border-radius: 8px;
font-weight: 500;
color: #303133;
text-align: center;
border-left: 4px solid #409eff;
}
.layer.outer .layer-box {
border-left-color: #67c23a;
}
.layer.middle .layer-box {
border-left-color: #e6a23c;
}
.layer.inner .layer-box {
border-left-color: #409eff;
}
.dependency-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 0;
}
.arrow-line {
width: 2px;
height: 12px;
background: #dcdfe6;
}
.arrow-head {
color: #909399;
font-size: 11px;
margin-top: 2px;
}
.principle-box {
background: linear-gradient(135deg, #e6f7ff 0%, #f0f7ff 100%);
border-radius: 10px;
padding: 20px;
border-left: 4px solid #1890ff;
}
.principle-title {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
}
.principle-content p {
margin: 0 0 12px 0;
color: #595959;
font-size: 13px;
line-height: 1.6;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.rule-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
background: white;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.rule-icon {
font-size: 16px;
flex-shrink: 0;
}
.rule-text {
flex: 1;
}
.rule-text strong {
color: #1a1a2e;
font-size: 13px;
}
.rule-desc {
color: #8c8c8c;
font-size: 12px;
margin-top: 2px;
}
.direction-diagram {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.direction-diagram h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.diagram-content {
overflow-x: auto;
}
.diagram-code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
color: #595959;
margin: 0;
white-space: pre;
}
@media (max-width: 768px) {
.model-cards {
grid-template-columns: 1fr;
}
.comparison-tabs {
flex-direction: column;
}
.tab-btn {
width: 100%;
text-align: center;
}
}
</style>
@@ -0,0 +1,501 @@
<template>
<div class="domain-model-demo">
<div class="demo-header">
<h4>📦 Domain 领域模型设计</h4>
<p class="subtitle">Domain 是业务概念的载体所有层的依赖基础</p>
</div>
<!-- 领域模型对比 -->
<div class="model-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="comparison-content">
<!-- 贫血模型 vs 充血模型 -->
<div v-if="currentTab === 'comparison'" class="tab-panel">
<div class="model-cards">
<div class="model-card anemic">
<div class="card-header">
<span class="card-icon">📄</span>
<span class="card-title">贫血模型 (Anemic)</span>
<span class="card-badge">传统做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">Entity只有 getter/setter</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 只有 getter/setter,没有业务逻辑
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 其他 getter/setter
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">Service所有业务逻辑都在这里</div>
<pre><code>@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
// 贫血模型:业务逻辑散落在 Service 里
if (order.getStatus() == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (order.getStatus() == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
// 修改状态
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}</code></pre>
</div>
<div class="problems">
<div class="problem-title">😫 贫血模型的问题</div>
<ul>
<li><strong>违背面向对象</strong>对象只有数据没有行为变成了 "数据结构"</li>
<li><strong>逻辑分散</strong>同样的业务规则可能在多个 Service 重复</li>
<li><strong>难以维护</strong>改一个规则要找所有用到的地方</li>
</ul>
</div>
</div>
</div>
<div class="model-card rich">
<div class="card-header">
<span class="card-icon">🧠</span>
<span class="card-title">充血模型 (Rich Domain)</span>
<span class="card-badge tag-green">推荐做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">Entity包含业务逻辑</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 🎯 业务行为封装在实体里
/**
* 取消订单
*/
public void cancel() {
// 状态校验内聚在方法里
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (this.status == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
this.status = OrderStatus.CANCELLED;
// 可以触发领域事件
registerEvent(new OrderCancelledEvent(this.id));
}
/**
* 支付订单
*/
public void pay(Payment payment) {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不正确");
}
if (!payment.getAmount().equals(this.totalAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
this.paymentTime = LocalDateTime.now();
}
// ... 其他业务方法
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">Service只做协调不做业务判断</div>
<pre><code>@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public void cancelOrder(Long orderId) {
// 1. 加载聚合根
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// 2. 💡 调用领域对象的业务方法
// 业务规则封装在 Order 里,Service 只做协调
order.cancel();
// 3. 保存变更
orderRepository.save(order);
// 4. 发布领域事件
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}</code></pre>
</div>
<div class="benefits">
<div class="benefit-title">😊 充血模型的优势</div>
<ul>
<li><strong>符合面向对象</strong>数据和行为封装在一起是真正的 "对象"</li>
<li><strong>业务内聚</strong>规则跟着对象走改一处处处生效</li>
<li><strong>可复用可测试</strong>领域对象是纯内存对象单元测试不需要数据库</li>
<li><strong>表达力强</strong>order.cancel() orderService.cancel(order) 更自然</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 值对象 -->
<div v-else-if="currentTab === 'valueobject'" class="tab-panel">
<div class="value-object-content">
<div class="concept-intro">
<h5>💎 什么是值对象Value Object</h5>
<p>值对象是没有唯一标识不可变的对象它描述了某种特征或属性两个值对象如果所有属性相等就被认为是同一个对象</p>
</div>
<div class="vo-examples">
<div class="example-card">
<div class="example-title">📍 地址 Address</div>
<pre><code>// 值对象:不可变、无 ID
public record Address(
String province, // 省
String city, // 市
String district, // 区
String street, // 街道
String zipCode // 邮编
) {
// 值对象的方法通常是转换或计算
public String toDisplayString() {
return String.format("%s%s%s%s",
province, city, district, street);
}
// 校验逻辑
public boolean isValid() {
return StringUtils.isNotBlank(province)
&& StringUtils.isNotBlank(city);
}
}
// 使用:地址相等只要属性相同
Address addr1 = new Address("广东", "深圳", "南山", "科技园", "518000");
Address addr2 = new Address("广东", "深圳", "南山", "科技园", "518000");
System.out.println(addr1.equals(addr2)); // true - 值对象比较的是值</code></pre>
</div>
<div class="example-card">
<div class="example-title">💰 金钱 Money</div>
<pre><code>// 金钱是经典的值对象
public record Money(
BigDecimal amount,
Currency currency
) {
// 工厂方法
public static Money of(BigDecimal amount, String currencyCode) {
return new Money(amount, Currency.getInstance(currencyCode));
}
public static Money yuan(BigDecimal amount) {
return new Money(amount, Currency.getInstance("CNY"));
}
// 值对象的核心:运算返回新的值对象
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), currency);
}
public boolean isGreaterThan(Money other) {
return this.amount.compareTo(other.amount) > 0;
}
// 格式化显示
public String toDisplayString() {
return currency.getSymbol() + amount.setScale(2, RoundingMode.HALF_UP);
}
}
// 使用示例
Money price = Money.yuan(new BigDecimal("199.99"));
Money shipping = Money.yuan(new BigDecimal("10.00"));
Money discount = Money.yuan(new BigDecimal("20.00"));
Money total = price.add(shipping).add(discount.negate());
System.out.println(total.toDisplayString()); // ¥189.99</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentTab = ref('comparison')
const tabs = [
{ id: 'comparison', name: '贫血 vs 充血' },
{ id: 'valueobject', name: '值对象设计' }
]
</script>
<style scoped>
.domain-model-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.model-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.model-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.model-card {
background: #f8f9fa;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e4e7ed;
}
.model-card.anemic {
border-left: 4px solid #ff4d4f;
}
.model-card.rich {
border-left: 4px solid #52c41a;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #e4e7ed;
}
.card-icon {
font-size: 20px;
}
.card-title {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.card-badge {
margin-left: auto;
padding: 2px 8px;
background: #ff4d4f;
color: white;
border-radius: 10px;
font-size: 11px;
}
.card-badge.tag-green {
background: #52c41a;
}
.card-content {
padding: 16px;
}
.code-section {
margin-bottom: 16px;
}
.code-label {
font-size: 11px;
color: #909399;
margin-bottom: 6px;
font-weight: 500;
}
.code-section pre {
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 0;
}
.code-section code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.5;
}
.problems, .benefits {
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.problems {
background: #fff1f0;
border-left: 3px solid #ff4d4f;
}
.benefits {
background: #f6ffed;
border-left: 3px solid #52c41a;
}
.problem-title, .benefit-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
}
.problem-title {
color: #cf1322;
}
.benefit-title {
color: #389e0d;
}
.problems ul, .benefits ul {
margin: 0;
padding-left: 16px;
}
.problems li, .benefits li {
margin: 6px 0;
font-size: 12px;
line-height: 1.5;
color: #595959;
}
@media (max-width: 1024px) {
.model-cards {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,338 @@
<template>
<div class="dto-flow-demo">
<div class="demo-header">
<h4>🔄 DTO 流转数据在不同层之间的转换</h4>
<p class="subtitle">DTOData Transfer Object是层与层之间传递数据的载体</p>
</div>
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-step">
<div class="step-title">Controller </div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 接收 Request DTO</span>
</div>
<div class="code-line">
<span class="keyword">public</span> ResponseEntity&lt;UserDTO&gt; createUser(
</div>
<div class="code-line">
&nbsp;&nbsp;@RequestBody <span class="highlight">@Valid UserCreateRequest request</span>
</div>
<div class="code-line">
) { ... }
</div>
</div>
</div>
<div class="flow-arrow"> 转换为 Service 需要的参数</div>
<div class="flow-step">
<div class="step-title">Service </div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 业务处理</span>
</div>
<div class="code-line">
<span class="keyword">public</span> UserDTO createUser(UserCreateParam param) {
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="comment">// 转换为 Entity</span>
</div>
<div class="code-line">
&nbsp;&nbsp;User user = <span class="highlight">param.toEntity()</span>;
</div>
<div class="code-line">
&nbsp;&nbsp;userRepository.save(user);
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="keyword">return</span> <span class="highlight">UserDTO.from(user)</span>;
</div>
<div class="code-line">
}
</div>
</div>
</div>
<div class="flow-arrow"> 转换为 Repository 需要的 Entity</div>
<div class="flow-step">
<div class="step-title">Repository </div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 数据持久化</span>
</div>
<div class="code-line">
<span class="keyword">public interface</span> UserRepository
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="keyword">extends</span> JpaRepository&lt;<span class="highlight">User</span>, Long&gt; {
</div>
<div class="code-line">
}
</div>
</div>
</div>
<div class="flow-arrow"> 返回 Entity转换为 DTO</div>
<div class="flow-step">
<div class="step-title">返回给客户端</div>
<div class="step-code">
<div class="code-line">
<span class="comment">// Response DTO</span>
</div>
<div class="code-line">
{
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"id"</span>: 10001,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"username"</span>: <span class="string">"张三"</span>,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"email"</span>: <span class="string">"zhangsan@example.com"</span>,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"createdAt"</span>: <span class="string">"2024-01-15T10:30:00Z"</span>
</div>
<div class="code-line">
}
</div>
</div>
</div>
</div>
<!-- 不同层 DTO 对比 -->
<div class="dto-comparison">
<h5>📋 不同层的 DTO 职责</h5>
<div class="comparison-table">
<div class="table-header">
<div class="col-layer">层级</div>
<div class="col-dto">DTO 类型</div>
<div class="col-purpose">职责</div>
<div class="col-example">示例</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag controller">Controller</span>
</div>
<div class="col-dto">
Request / Response DTO
</div>
<div class="col-purpose">
定义 API 契约参数校验序列化
</div>
<div class="col-example">
<code>UserCreateRequest</code>
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag service">Service</span>
</div>
<div class="col-dto">
Param / Result DTO
</div>
<div class="col-purpose">
封装业务方法参数解耦 Controller Service
</div>
<div class="col-example">
<code>UserCreateParam</code>
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag repository">Repository</span>
</div>
<div class="col-dto">
Entity / DO
</div>
<div class="col-purpose">
映射数据库表结构ORM 映射
</div>
<div class="col-example">
<code>UserEntity</code>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('conversion')
</script>
<style scoped>
.dto-flow-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.flow-diagram {
background: white;
border-radius: 10px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.flow-step {
background: #f8f9fa;
border-radius: 8px;
overflow: hidden;
border-left: 4px solid #409eff;
}
.flow-step:nth-child(odd) {
border-left-color: #67c23a;
}
.step-title {
padding: 12px 16px;
background: white;
font-weight: 600;
color: #303133;
font-size: 14px;
border-bottom: 1px solid #ebeef5;
}
.step-code {
padding: 16px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.6;
}
.code-line {
padding: 2px 0;
}
.comment {
color: #6a9955;
}
.keyword {
color: #569cd6;
}
.highlight {
background: #fff3cd;
padding: 2px 4px;
border-radius: 3px;
color: #856404;
}
.string {
color: #ce9178;
}
.flow-arrow {
text-align: center;
padding: 12px;
color: #909399;
font-size: 13px;
font-weight: 500;
}
.dto-comparison {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.dto-comparison h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.comparison-table {
overflow-x: auto;
}
.table-header, .table-row {
display: grid;
grid-template-columns: 100px 150px 1fr 120px;
gap: 12px;
padding: 12px;
align-items: center;
}
.table-header {
background: #f5f7fa;
border-radius: 8px;
font-weight: 600;
color: #303133;
font-size: 13px;
}
.table-row {
border-bottom: 1px solid #ebeef5;
font-size: 12px;
color: #606266;
}
.table-row:last-child {
border-bottom: none;
}
.layer-tag {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
display: inline-block;
}
.layer-tag.controller {
background: #f0f9ff;
color: #1890ff;
}
.layer-tag.service {
background: #fff7e6;
color: #fa8c16;
}
.layer-tag.repository {
background: #f6ffed;
color: #52c41a;
}
@media (max-width: 768px) {
.table-header, .table-row {
grid-template-columns: 1fr;
gap: 8px;
}
.table-header {
display: none;
}
}
</style>
@@ -0,0 +1,352 @@
<template>
<div class="layered-architecture-demo">
<div class="architecture-container">
<!-- 客户端 -->
<div class="client-layer">
<div class="layer-box client">
<div class="layer-icon">🌐</div>
<div class="layer-title">客户端</div>
<div class="layer-desc">Web / App / 小程序</div>
</div>
<div class="arrow-down"> HTTP/HTTPS</div>
</div>
<!-- 后端分层 -->
<div class="backend-layers">
<!-- Controller -->
<div
class="layer-box controller"
:class="{ active: activeLayer === 'controller' }"
@click="setActiveLayer('controller')"
>
<div class="layer-header">
<span class="layer-icon">🎮</span>
<span class="layer-name">Controller</span>
<span class="layer-badge">入口</span>
</div>
<div class="layer-content">
<div class="duty">职责接收请求参数校验调用 Service</div>
<div class="tech">技术Spring MVC / Gin / Echo</div>
</div>
</div>
<div class="arrow-down"> 调用</div>
<!-- Service -->
<div
class="layer-box service"
:class="{ active: activeLayer === 'service' }"
@click="setActiveLayer('service')"
>
<div class="layer-header">
<span class="layer-icon"></span>
<span class="layer-name">Service</span>
<span class="layer-badge">业务核心</span>
</div>
<div class="layer-content">
<div class="duty">职责业务逻辑编排事务管理跨模块协调</div>
<div class="tech">技术纯代码逻辑 / 无框架依赖</div>
</div>
</div>
<div class="arrow-down"> 调用</div>
<!-- Repository -->
<div
class="layer-box repository"
:class="{ active: activeLayer === 'repository' }"
@click="setActiveLayer('repository')"
>
<div class="layer-header">
<span class="layer-icon">🗄</span>
<span class="layer-name">Repository</span>
<span class="layer-badge">数据访问</span>
</div>
<div class="layer-content">
<div class="duty">职责数据持久化查询封装ORM 映射</div>
<div class="tech">技术MyBatis / GORM / Hibernate</div>
</div>
</div>
<div class="arrow-down"> SQL</div>
<!-- Domain -->
<div
class="layer-box domain"
:class="{ active: activeLayer === 'domain' }"
@click="setActiveLayer('domain')"
>
<div class="layer-header">
<span class="layer-icon">📦</span>
<span class="layer-name">Domain / Model</span>
<span class="layer-badge">领域模型</span>
</div>
<div class="layer-content">
<div class="duty">职责实体定义业务规则值对象</div>
<div class="tech">技术POJO / Struct / Class</div>
</div>
</div>
<div class="arrow-down"> 持久化</div>
<!-- 数据库 -->
<div class="layer-box database">
<div class="layer-icon">💾</div>
<div class="layer-title">数据库</div>
<div class="layer-desc">MySQL / PostgreSQL / MongoDB</div>
</div>
</div>
<!-- 右侧说明面板 -->
<div class="info-panel" v-if="activeLayer">
<h4>{{ layerInfo.title }}</h4>
<p>{{ layerInfo.description }}</p>
<div class="analogy">
<strong>💡 类比</strong>{{ layerInfo.analogy }}
</div>
<div class="common-mistakes">
<strong> 常见错误</strong>
<ul>
<li v-for="mistake in layerInfo.mistakes" :key="mistake">{{ mistake }}</li>
</ul>
</div>
</div>
</div>
<!-- 底部交互提示 -->
<div class="interaction-hint">
💡 点击各层查看详细说明 | 实际调用流向从上到下依赖从下到上
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLayer = ref('')
const setActiveLayer = (layer) => {
activeLayer.value = activeLayer.value === layer ? '' : layer
}
const layerInfo = computed(() => {
const infoMap = {
controller: {
title: 'Controller 层 - 请求的"门童"',
description: 'Controller 是系统的入口,负责接收 HTTP 请求、解析参数、进行基础校验,然后调用 Service 层处理业务。',
analogy: '就像餐厅的门童,负责迎接客人(接收请求)、检查预约(参数校验)、引导入座(路由到对应服务),但不负责做菜。',
mistakes: [
'在 Controller 里写业务逻辑(应该放在 Service)',
'直接操作数据库(应该调用 Repository',
'不做参数校验,导致脏数据流入系统'
]
},
service: {
title: 'Service 层 - 业务逻辑的"厨师"',
description: 'Service 是系统的核心,负责编排业务逻辑、管理事务、协调多个 Repository。这一层应该包含所有的业务规则和流程。',
analogy: '就像餐厅的厨师,负责按照菜谱(业务规则)做菜,需要协调各种食材(数据),把控菜品质量(业务正确性)。',
mistakes: [
'Service 之间互相调用,形成循环依赖',
'在 Service 里直接写 SQL(应该放在 Repository',
'一个 Service 方法过长,包含多个业务场景(应该拆分成多个方法)'
]
},
repository: {
title: 'Repository 层 - 数据的"仓管"',
description: 'Repository 负责与数据库交互,封装所有的 CRUD 操作。上层不需要关心具体的数据库类型和 SQL 语句。',
analogy: '就像餐厅的仓管员,负责从仓库(数据库)取食材、存放剩余食材,厨师(Service)只需要告诉他要什么,不需要知道仓库在哪。',
mistakes: [
'在 Repository 里写业务逻辑(应该只负责数据访问)',
'直接返回数据库实体给前端(应该转换为 DTO)',
'一个 Repository 操作多个表(应该拆分到不同 Repository'
]
},
domain: {
title: 'Domain 层 - 业务概念的"蓝图"',
description: 'Domain 定义了系统中的实体、值对象、业务规则。它是所有层的依赖基础,但不依赖任何其他层。',
analogy: '就像餐厅的菜单和菜品标准,定义了什么是"宫保鸡丁"、用什么食材、什么口味。所有厨师都要按照这个标准来做。',
mistakes: [
'Domain 对象里包含持久化注解(应该保持纯净)',
'在 Domain 里写业务逻辑(业务逻辑应该在 Service)',
'Domain 对象之间循环依赖'
]
}
}
return infoMap[activeLayer.value] || {}
})
</script>
<style scoped>
.layered-architecture-demo {
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.architecture-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.client-layer {
display: flex;
flex-direction: column;
align-items: center;
}
.backend-layers {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-box {
padding: 16px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.layer-box:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.layer-box.active {
border-color: #409eff;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
}
/* 各层颜色主题 */
.controller { border-left: 4px solid #67c23a; }
.service { border-left: 4px solid #e6a23c; }
.repository { border-left: 4px solid #409eff; }
.domain { border-left: 4px solid #909399; }
.client { border-left: 4px solid #f56c6c; }
.database { border-left: 4px solid #8e44ad; }
.layer-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.layer-icon {
font-size: 20px;
}
.layer-name {
font-weight: 600;
font-size: 15px;
color: #303133;
}
.layer-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
background: #f0f2f5;
color: #606266;
}
.layer-content {
padding-left: 30px;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.duty, .tech {
margin: 4px 0;
}
.arrow-down {
text-align: center;
padding: 6px;
font-size: 12px;
color: #909399;
}
.info-panel {
width: 320px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: sticky;
top: 20px;
}
.info-panel h4 {
margin: 0 0 12px 0;
color: #303133;
font-size: 16px;
padding-bottom: 10px;
border-bottom: 2px solid #409eff;
}
.info-panel p {
margin: 0 0 16px 0;
color: #606266;
font-size: 13px;
line-height: 1.7;
}
.analogy, .common-mistakes {
margin: 12px 0;
padding: 12px;
border-radius: 6px;
font-size: 12px;
line-height: 1.6;
}
.analogy {
background: #f0f9ff;
border-left: 3px solid #409eff;
color: #1d4ed8;
}
.common-mistakes {
background: #fff2f0;
border-left: 3px solid #ff4d4f;
color: #cf1322;
}
.common-mistakes ul {
margin: 6px 0 0 0;
padding-left: 16px;
}
.common-mistakes li {
margin: 4px 0;
}
.interaction-hint {
text-align: center;
padding: 16px;
margin-top: 16px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 8px;
color: #389e0d;
font-size: 13px;
}
@media (max-width: 1024px) {
.architecture-container {
flex-direction: column;
}
.info-panel {
width: 100%;
position: static;
}
}
</style>
@@ -0,0 +1,698 @@
<template>
<div class="service-layer-demo">
<div class="demo-header">
<h4> Service 业务逻辑的"指挥家"</h4>
<p class="subtitle">Service 层编排业务逻辑协调多个 Repository管理事务边界</p>
</div>
<!-- 场景选择器 -->
<div class="scenario-selector">
<div class="selector-label">选择业务场景</div>
<div class="scenario-buttons">
<button
v-for="scenario in scenarios"
:key="scenario.id"
:class="['scenario-btn', { active: currentScenario === scenario.id }]"
@click="currentScenario = scenario.id"
>
{{ scenario.name }}
</button>
</div>
</div>
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-header">
<span class="flow-title">{{ currentScenarioData.title }}</span>
<span class="flow-desc">{{ currentScenarioData.description }}</span>
</div>
<div class="flow-steps">
<div
v-for="(step, index) in currentScenarioData.steps"
:key="index"
class="flow-step"
:class="{ 'has-sub-steps': step.subSteps }"
@click="toggleStep(index)"
>
<div class="step-header">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-info">
<div class="step-name">{{ step.name }}</div>
<div class="step-layer">{{ step.layer }}</div>
</div>
<div v-if="step.subSteps" class="expand-icon">
{{ expandedSteps.includes(index) ? '' : '' }}
</div>
</div>
<div v-if="step.code" class="step-code">
<pre><code>{{ step.code }}</code></pre>
</div>
<!-- 子步骤事务管理 -->
<div v-if="step.subSteps && expandedSteps.includes(index)" class="sub-steps">
<div
v-for="(subStep, subIndex) in step.subSteps"
:key="subIndex"
class="sub-step"
:class="subStep.status"
>
<div class="sub-step-icon">{{ subStep.icon }}</div>
<div class="sub-step-content">
<div class="sub-step-name">{{ subStep.name }}</div>
<div class="sub-step-desc">{{ subStep.desc }}</div>
</div>
<div class="sub-step-status">{{ subStep.statusText }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Service 设计原则 -->
<div class="design-principles">
<h5>🎯 Service 层设计原则</h5>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
>
<div class="principle-icon">{{ principle.icon }}</div>
<div class="principle-title">{{ principle.title }}</div>
<div class="principle-desc">{{ principle.desc }}</div>
<div class="principle-example">
<code>{{ principle.example }}</code>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentScenario = ref('order')
const expandedSteps = ref([])
const scenarios = [
{ id: 'order', name: '下单流程' },
{ id: 'refund', name: '退款处理' },
{ id: 'report', name: '报表生成' }
]
const scenarioData = {
order: {
title: '🛒 电商下单流程',
description: '用户下单涉及库存扣减、订单创建、支付记录等多个操作,需要保证事务一致性',
steps: [
{
name: '参数校验与DTO转换',
layer: 'Controller',
code: `@PostMapping("/orders")
public ResponseEntity&lt;OrderDTO&gt; createOrder(
@RequestBody @Valid CreateOrderRequest request
) {
// 调用 Service
OrderDTO order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}`
},
{
name: '业务逻辑编排(事务管理)',
layer: 'Service',
code: `@Service
@Transactional // 关键:事务管理
public class OrderService {
public OrderDTO createOrder(CreateOrderRequest request) {
// 1. 检查库存
inventoryService.checkAndDeduct(request.getSkuId(),
request.getQuantity());
// 2. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(calculateTotal(request));
orderRepository.save(order);
// 3. 创建支付记录
Payment payment = createPayment(order);
paymentRepository.save(payment);
// 任一失败都会回滚
return convertToDTO(order);
}
}`,
subSteps: [
{
icon: '✅',
name: '检查并扣减库存',
desc: '确保库存充足,预先锁定',
status: 'success',
statusText: '成功'
},
{
icon: '📝',
name: '创建订单记录',
desc: '生成订单主表数据',
status: 'success',
statusText: '成功'
},
{
icon: '💳',
name: '创建支付记录',
desc: '初始化待支付状态',
status: 'success',
statusText: '成功'
},
{
icon: '🔄',
name: '事务提交',
desc: '所有操作原子性提交',
status: 'success',
statusText: '已提交'
}
]
},
{
name: '数据持久化',
layer: 'Repository',
code: `public interface OrderRepository extends JpaRepository&lt;Order, Long&gt; {
// 基本的 CRUD 已内置
}
// 实际执行:INSERT INTO orders (...) VALUES (...)`
}
]
},
refund: {
title: '💰 退款处理流程',
description: '退款涉及订单状态变更、支付原路返回、库存回滚等操作',
steps: [
{
name: '接收退款申请',
layer: 'Controller',
code: `@PostMapping("/orders/{orderId}/refund")
public ResponseEntity&lt;RefundDTO&gt; applyRefund(
@PathVariable Long orderId,
@RequestBody @Valid RefundRequest request
) {
RefundDTO refund = refundService.processRefund(orderId, request);
return ResponseEntity.ok(refund);
}`
},
{
name: '退款业务处理',
layer: 'Service',
code: `@Service
@Transactional
public class RefundService {
public RefundDTO processRefund(Long orderId, RefundRequest request) {
// 1. 验证订单状态
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("订单不存在"));
if (order.getStatus() != OrderStatus.PAID) {
throw new InvalidOrderStateException("订单状态不允许退款");
}
// 2. 计算退款金额
BigDecimal refundAmount = calculateRefundAmount(order, request);
// 3. 调用支付渠道退款
PaymentRefundResult result = paymentService.refund(
order.getPaymentNo(),
refundAmount,
request.getReason()
);
// 4. 更新订单状态
order.setStatus(OrderStatus.REFUNDING);
orderRepository.save(order);
// 5. 保存退款记录
RefundRecord record = new RefundRecord();
record.setOrderId(orderId);
record.setAmount(refundAmount);
record.setReason(request.getReason());
record.setStatus(RefundStatus.PROCESSING);
refundRecordRepository.save(record);
// 6. 异步恢复库存
inventoryService.restoreStockAsync(order.getItems());
return convertToDTO(record);
}
}`,
subSteps: [
{ icon: '🔍', name: '验证订单状态', desc: '检查订单是否存在且可退款', status: 'success', statusText: '通过' },
{ icon: '💰', name: '计算退款金额', desc: '根据规则计算应退金额', status: 'success', statusText: '完成' },
{ icon: '🏦', name: '调用支付渠道', desc: '请求第三方支付退款', status: 'success', statusText: '处理中' },
{ icon: '📝', name: '更新订单状态', desc: '标记为退款中', status: 'success', statusText: '已更新' },
{ icon: '📊', name: '保存退款记录', desc: '记录退款流水', status: 'success', statusText: '已保存' },
{ icon: '🔄', name: '异步恢复库存', desc: '后台恢复商品库存', status: 'success', statusText: '已提交' }
]
}
]
},
report: {
title: '📊 报表生成流程',
description: '复杂的报表通常涉及多个数据源查询、数据聚合计算、异步导出等',
steps: [
{
name: '接收报表请求',
layer: 'Controller',
code: `@GetMapping("/reports/sales")
public ResponseEntity<ReportTaskDTO> generateSalesReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate endDate,
@RequestParam(required = false) List<Long> regionIds
) {
// 大报表采用异步生成
ReportTaskDTO task = reportService.createReportTask(
ReportType.SALES, startDate, endDate, regionIds
);
return ResponseEntity.accepted().body(task);
}`
},
{
name: '复杂报表业务编排',
layer: 'Service',
code: `@Service
public class ReportService {
@Async("reportExecutor")
public void generateReportAsync(Long taskId) {
ReportTask task = reportTaskRepository.findById(taskId)
.orElseThrow();
try {
task.setStatus(TaskStatus.RUNNING);
reportTaskRepository.save(task);
// 1. 从多个数据源聚合数据
SalesReportData data = aggregateSalesData(task);
// 2. 计算各项指标
calculateMetrics(data);
// 3. 生成图表数据
generateChartData(data);
// 4. 导出为 Excel
String fileUrl = exportToExcel(data, task);
task.setStatus(TaskStatus.COMPLETED);
task.setFileUrl(fileUrl);
task.setCompletedAt(LocalDateTime.now());
} catch (Exception e) {
task.setStatus(TaskStatus.FAILED);
task.setErrorMessage(e.getMessage());
}
reportTaskRepository.save(task);
}
private SalesReportData aggregateSalesData(ReportTask task) {
// 协调多个 Repository 查询数据
List<Order> orders = orderRepository
.findByCreatedAtBetween(task.getStartDate(), task.getEndDate());
List<Payment> payments = paymentRepository
.findByPaidAtBetween(task.getStartDate(), task.getEndDate());
List<RefundRecord> refunds = refundRecordRepository
.findByCreatedAtBetween(task.getStartDate(), task.getEndDate());
// 数据聚合逻辑...
return new SalesReportData(orders, payments, refunds);
}
}`,
subSteps: [
{ icon: '📥', name: '从多个数据源查询', desc: 'Orders/Payments/Refunds', status: 'success', statusText: '已查询' },
{ icon: '🔄', name: '数据聚合与清洗', desc: '关联数据、处理缺失值', status: 'success', statusText: '已完成' },
{ icon: '📊', name: '计算业务指标', desc: 'GMV、订单数、客单价等', status: 'success', statusText: '已计算' },
{ icon: '📈', name: '生成图表数据', desc: '趋势图、占比图数据结构', status: 'success', statusText: '已生成' },
{ icon: '📄', name: '导出 Excel 文件', desc: '生成并上传至 OSS', status: 'success', statusText: '已完成' }
]
}
]
}
}
const currentScenarioData = computed(() => scenarioData[currentScenario.value])
const toggleStep = (index) => {
const i = expandedSteps.value.indexOf(index)
if (i > -1) {
expandedSteps.value.splice(i, 1)
} else {
expandedSteps.value.push(index)
}
}
const principles = [
{
id: 1,
icon: '🎯',
title: '单一职责',
desc: '一个 Service 类只负责一块业务领域',
example: 'UserService 只管用户,OrderService 只管订单'
},
{
id: 2,
icon: '🔄',
title: '事务边界',
desc: '在 Service 层声明式管理事务',
example: '@Transactional 放在 Service 方法上'
},
{
id: 3,
icon: '🔗',
title: '避免循环依赖',
desc: 'Service 之间不要互相调用',
example: 'A 调用 BB 又调用 A 会导致循环'
},
{
id: 4,
icon: '📦',
title: 'DTO 转换',
desc: '返回前转换为 DTO,不暴露实体',
example: 'return new UserDTO(user)'
}
]
</script>
<style scoped>
.service-layer-demo {
padding: 24px;
background: linear-gradient(135deg, #fff9f0 0%, #fff0e6 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.scenario-selector {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.selector-label {
font-weight: 600;
color: #303133;
font-size: 14px;
white-space: nowrap;
}
.scenario-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.scenario-btn {
padding: 8px 16px;
border: 1px solid #dcdfe6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: #606266;
transition: all 0.3s ease;
}
.scenario-btn:hover {
border-color: #e6a23c;
color: #e6a23c;
}
.scenario-btn.active {
background: #e6a23c;
border-color: #e6a23c;
color: white;
}
.flow-diagram {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.flow-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.flow-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.flow-desc {
font-size: 13px;
color: #909399;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.flow-step {
background: #f8f9fa;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid #e6a23c;
}
.flow-step:hover {
background: #fff8f0;
transform: translateX(4px);
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.step-number {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #e6a23c;
color: white;
border-radius: 50%;
font-size: 13px;
font-weight: 600;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.step-layer {
font-size: 12px;
color: #e6a23c;
margin-top: 2px;
}
.expand-icon {
color: #909399;
font-size: 12px;
}
.step-code {
padding: 0 16px 16px 56px;
}
.step-code pre {
margin: 0;
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
}
.step-code code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
}
.sub-steps {
padding: 0 16px 16px 56px;
}
.sub-step {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: white;
border-radius: 6px;
margin-bottom: 8px;
border-left: 3px solid #dcdfe6;
}
.sub-step.success {
border-left-color: #67c23a;
background: #f6ffed;
}
.sub-step-icon {
font-size: 16px;
}
.sub-step-content {
flex: 1;
}
.sub-step-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.sub-step-desc {
font-size: 11px;
color: #909399;
margin-top: 2px;
}
.sub-step-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: #f0f9ff;
color: #1890ff;
}
.sub-step.success .sub-step-status {
background: #f6ffed;
color: #52c41a;
}
.design-principles {
margin-top: 24px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.design-principles h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.principle-card {
padding: 14px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #e6a23c;
}
.principle-icon {
font-size: 20px;
margin-bottom: 6px;
}
.principle-title {
font-weight: 600;
color: #303133;
font-size: 13px;
margin-bottom: 4px;
}
.principle-desc {
color: #606266;
font-size: 11px;
margin-bottom: 8px;
line-height: 1.5;
}
.principle-example {
background: #2d2d2d;
padding: 8px;
border-radius: 4px;
overflow-x: auto;
}
.principle-example code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.4;
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: 1fr;
}
.scenario-selector {
flex-direction: column;
align-items: flex-start;
}
.step-code {
padding-left: 16px;
}
}
</style>