feat: update documentation and component demos for backend layered architecture

- Add new LanguageScopeDemo component for backend languages overview
- Refactor and simplify existing demo components (ControllerLayerDemo, DtoFlowDemo, DependencyDirectionDemo)
- Update .gitignore to exclude .claude/skills directory
- Modify backend-related sections in documentation from "后端与全栈" to "后端开发"
- Add new backend layered architecture demo components (CleanArchitectureDemo, DependencyDirectionDemo)
- Improve documentation structure and content for stage-3 core skills
- Fix component initialization timing in CompileVsInterpretDemo and RateLimiterDemo
- Add design style prompt reference in frontend documentation
This commit is contained in:
sanbuphy
2026-03-01 12:28:47 +08:00
parent d8eb93663d
commit dc8b5773f1
22 changed files with 2660 additions and 5288 deletions
@@ -1,398 +1,154 @@
<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 class="layered-arch-demo">
<div class="header">
<div class="title">后端四层架构总览</div>
<div class="subtitle">点击各层查看详细说明</div>
</div>
<div class="main">
<div class="layers">
<div class="client-box">客户端 (Web / App)</div>
<div class="arrow"> HTTP</div>
<div
v-for="layer in layers"
:key="layer.id"
:class="['layer-box', layer.id, { active: active === layer.id }]"
@click="active = active === layer.id ? '' : layer.id"
>
<div class="layer-header">
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-badge">{{ layer.badge }}</span>
</div>
<div class="layer-duty">{{ layer.duty }}</div>
</div>
<div class="arrow-down">
HTTP/HTTPS
</div>
<div class="arrow"> SQL</div>
<div class="client-box db">数据库 (MySQL / PostgreSQL)</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
v-if="activeLayer"
class="info-panel"
>
<h4>{{ layerInfo.title }}</h4>
<p>{{ layerInfo.description }}</p>
<div class="analogy">
<strong>💡 类比</strong>{{ layerInfo.analogy }}
</div>
<div class="common-mistakes">
<strong> 常见错误</strong>
<div v-if="active" class="info-panel">
<div class="info-title">{{ activeInfo.title }}</div>
<p>{{ activeInfo.desc }}</p>
<div class="info-analogy">{{ activeInfo.analogy }}</div>
<div class="info-mistakes">
<strong>常见错误</strong>
<ul>
<li
v-for="mistake in layerInfo.mistakes"
:key="mistake"
>
{{ mistake }}
</li>
<li v-for="m in activeInfo.mistakes" :key="m">{{ m }}</li>
</ul>
</div>
</div>
</div>
<!-- 底部交互提示 -->
<div class="interaction-hint">
💡 点击各层查看详细说明 | 实际调用流向从上到下依赖从下到上
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLayer = ref('')
const active = ref('')
const setActiveLayer = (layer) => {
activeLayer.value = activeLayer.value === layer ? '' : layer
const layers = [
{ id: 'controller', name: 'Controller', badge: '入口', duty: '接收请求、参数校验、调用 Service' },
{ id: 'service', name: 'Service', badge: '业务核心', duty: '业务逻辑编排、事务管理、跨模块协调' },
{ id: 'repository', name: 'Repository', badge: '数据访问', duty: '数据持久化、查询封装、ORM 映射' },
{ id: 'domain', name: 'Domain', badge: '领域模型', duty: '实体定义、业务规则、值对象' }
]
const infoMap = {
controller: {
title: 'Controller 层 — 请求的"门童"',
desc: '负责接收 HTTP 请求、解析参数、进行基础校验,然后调用 Service 层处理业务。',
analogy: '就像餐厅的门童,负责迎接客人、检查预约、引导入座,但不负责做菜。',
mistakes: ['在 Controller 里写业务逻辑', '直接操作数据库', '不做参数校验']
},
service: {
title: 'Service 层 — 业务逻辑的"厨师"',
desc: '编排业务逻辑、管理事务、协调多个 Repository。包含所有的业务规则和流程。',
analogy: '就像餐厅的厨师,按照菜谱做菜,协调各种食材,把控菜品质量。',
mistakes: ['Service 之间循环依赖', '直接写 SQL', '单个方法过长包含多个业务场景']
},
repository: {
title: 'Repository 层 — 数据的"仓管"',
desc: '封装所有数据访问逻辑,上层不需要关心具体的数据库类型和 SQL 语句。',
analogy: '就像仓管员,负责从仓库取食材、存放剩余食材,厨师只需说要什么。',
mistakes: ['在 Repository 里写业务逻辑', '直接返回实体给前端', '一个 Repository 操作多个表']
},
domain: {
title: 'Domain 层 — 业务概念的"蓝图"',
desc: '定义实体、值对象、业务规则。是所有层的依赖基础,但不依赖任何其他层。',
analogy: '就像菜单和菜品标准,定义了什么是"宫保鸡丁"、用什么食材、什么口味。',
mistakes: ['Domain 包含持久化注解', '在 Domain 里写数据库操作', 'Domain 对象之间循环依赖']
}
}
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] || {}
})
const activeInfo = computed(() => infoMap[active.value] || {})
</script>
<style scoped>
.layered-architecture-demo {
.layered-arch-demo {
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
background: var(--vp-c-bg-soft);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.header { text-align: center; margin-bottom: 20px; }
.title { font-size: 16px; font-weight: 600; color: var(--vp-c-text-1); }
.subtitle { font-size: 13px; color: var(--vp-c-text-3); margin-top: 4px; }
.architecture-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.main { display: flex; gap: 20px; align-items: flex-start; }
.layers { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.client-layer {
display: flex;
flex-direction: column;
align-items: center;
}
.backend-layers {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.client-box {
padding: 12px; text-align: center; border-radius: 8px;
background: var(--vp-c-bg); color: var(--vp-c-text-2);
font-size: 13px; border: 1px solid var(--vp-c-divider);
}
.client-box.db { border-left: 3px solid #8b5cf6; }
.arrow { text-align: center; color: var(--vp-c-text-3); font-size: 12px; padding: 2px; }
.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;
padding: 14px; border-radius: 8px; cursor: pointer;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-left: 3px solid var(--vp-c-divider);
transition: all .2s;
}
.layer-box:hover { box-shadow: 0 2px 8px rgba(0,0,0,.06); }
.layer-box.active { border-color: var(--vp-c-brand-1); box-shadow: 0 0 0 2px var(--vp-c-brand-soft); }
.layer-box.controller { border-left-color: #10b981; }
.layer-box.service { border-left-color: #f59e0b; }
.layer-box.repository { border-left-color: #3b82f6; }
.layer-box.domain { border-left-color: #6b7280; }
.layer-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.layer-name { font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); }
.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;
padding: 1px 8px; border-radius: 10px; font-size: 11px;
background: var(--vp-c-bg-soft); color: var(--vp-c-text-3);
}
.layer-duty { font-size: 12px; color: var(--vp-c-text-2); }
.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;
width: 300px; padding: 18px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
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-title { font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid var(--vp-c-brand-1); }
.info-panel p { font-size: 13px; color: var(--vp-c-text-2); line-height: 1.6; margin: 0 0 12px; }
.info-analogy {
padding: 10px; border-radius: 6px; font-size: 12px; line-height: 1.5;
background: var(--vp-c-brand-soft); color: var(--vp-c-text-1);
border-left: 3px solid var(--vp-c-brand-1); margin-bottom: 12px;
}
.info-panel p {
margin: 0 0 16px 0;
color: #606266;
font-size: 13px;
line-height: 1.7;
.info-mistakes {
padding: 10px; border-radius: 6px; font-size: 12px; line-height: 1.5;
background: var(--vp-c-danger-soft); color: var(--vp-c-text-1);
border-left: 3px solid var(--vp-c-danger-1);
}
.info-mistakes strong { font-size: 12px; }
.info-mistakes ul { margin: 6px 0 0; padding-left: 16px; }
.info-mistakes li { margin: 3px 0; }
.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: 6px;
color: #389e0d;
font-size: 13px;
}
@media (max-width: 1024px) {
.architecture-container {
flex-direction: column;
}
.info-panel {
width: 100%;
position: static;
}
@media (max-width: 768px) {
.main { flex-direction: column; }
.info-panel { width: 100%; position: static; }
}
</style>