Compare commits

...

43 Commits

Author SHA1 Message Date
f47e512d02 feat:更新.net网络连接文档 2026-08-19 10:54:27 +08:00
a5569f70ae feat:增加MeiliSearch文档 2026-08-16 17:21:36 +08:00
911429d591 feat:增加大模型工具使用说明文档 2026-06-29 11:01:32 +08:00
eadbf47c5f feat:增加RAG文档 2026-06-24 09:53:18 +08:00
9fb8c23518 feat:更新秒杀系统文档 2026-06-22 15:51:10 +08:00
2be39b4d81 feat:更新通用模块表情 2026-06-11 20:56:03 +08:00
b839e361ac feat:增加秒杀系统设计文档 2026-06-10 18:16:29 +08:00
b40d3f14a8 feat:增加TextToSql文档 2026-06-09 14:01:34 +08:00
46366ef73d feat:更新图片路径 2026-06-08 21:32:49 +08:00
597159298f feat:增加返回顶部组件 2026-06-08 16:31:20 +08:00
21414245d9 feat:增加个人博客系统文档 2026-06-08 16:08:00 +08:00
9c11e484f3 feat 2026-06-08 10:20:45 +08:00
b3c0452621 feat:增加VueUse文档 2026-06-05 17:36:30 +08:00
a64f20e84f feat:整理博客目录结构 2026-06-05 15:53:29 +08:00
a5976c9783 feat:增加SweetHub项目文档 2026-06-04 19:58:02 +08:00
845a29f476 feat:增加SpringBoot 线程与事件文档 2026-06-02 19:41:06 +08:00
a825ccdadb feat:更新埃学习文档 2026-06-01 19:58:05 +08:00
2777ef8676 feat:增加LangChain文档 2026-05-28 14:11:09 +08:00
6ab2f8a1f2 fix:修复打包错误的问题 2026-05-27 23:04:44 +08:00
46545ca0c2 feat:更新MySQL文档 2026-05-27 14:17:04 +08:00
efc1f5fbae feat:增加项目实战模块 2026-05-27 11:26:16 +08:00
8c9b3adc57 feat:更新文档架构 2026-05-20 11:26:38 +08:00
ad9a4d6806 feat:增加Flutter安装文档 2026-05-02 21:05:34 +08:00
e8081aa92a feat:更新docker部署文档 2026-04-29 11:11:20 +08:00
306f8df886 feat:更新网络编程文档 2026-04-23 19:42:24 +08:00
0ea90ccccf feat:更新openpobserver文档 2026-04-21 22:30:23 +08:00
94c1157260 feat:更新fluentbit文档 2026-04-21 20:02:23 +08:00
906e8ea5d5 feat:更新fluent-bit文档 2026-04-13 19:47:47 +08:00
fa8f0da32a feat:增加RustDesk文档 2026-03-24 22:05:08 +08:00
681aff8bd5 feat:增加Redis文档 2026-03-22 16:25:37 +08:00
a0c40be4b6 feat:增加Systemback文档 2026-03-12 22:00:12 +08:00
62d0b0515e feat:更新图片预览插件 2026-02-26 19:01:03 +08:00
24c3fded6c feat:更新Alist文档 2026-02-26 14:28:06 +08:00
e4717ac906 feat:增加XDRP文档 2026-02-25 19:45:28 +08:00
40b5f462fe feat:更新mybatis文档 2026-02-03 14:45:18 +08:00
cbe8821376 feat:增加视频压缩文档 2026-01-29 15:01:35 +08:00
b39d345da9 feat:增加企业微信接口博客 2026-01-23 17:08:40 +08:00
2549b696e1 feat:增加RestClient文档 2026-01-08 09:36:35 +08:00
4f07faaa64 feat:增加fastapi博客 2026-01-06 19:57:45 +08:00
b6669a1731 feat:更新springboot技巧文档 2026-01-05 22:23:41 +08:00
939cf05db2 feat:增加注解文档 2025-12-30 19:03:46 +08:00
66d565d65b feat:增加SpringAOP文档 2025-12-25 16:19:09 +08:00
3911a00a51 feat:更新MySQL博客 2025-12-21 17:17:42 +08:00
107 changed files with 7390 additions and 271 deletions

39
.idea/dataSources.local.xml generated Normal file
View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="WS-253.29346.143">
<data-source name="@localhost" uuid="0a5f547c-9d7e-48c2-8022-49fb896f0b20">
<database-info product="MySQL" version="8.4.9" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-j-9.5.0 (Revision: a7b3c94f50efbddb9f0dd69b3e0d1aaa25305cd6)" dbms="MYSQL" exact-version="8.4.9" exact-driver-version="9.5">
<extra-name-characters>$</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="lower" />
<secret-storage>master_key</secret-storage>
<user-name>root</user-name>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
</data-source>
<data-source name="192.168.1.7" uuid="d432891a-8082-4b1f-89b4-c55af2fe67de">
<database-info product="MySQL" version="8.0.31" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-j-9.5.0 (Revision: a7b3c94f50efbddb9f0dd69b3e0d1aaa25305cd6)" dbms="MYSQL" exact-version="8.0.31" exact-driver-version="9.5">
<extra-name-characters>$</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="exact" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>root</user-name>
<schema-mapping>
<introspection-scope>
<node kind="schema">
<name qname="@" />
<name qname="blog" />
<name qname="food_hub" />
<name qname="sweet_hut" />
<name qname="xxl_job" />
</node>
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>

19
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="0a5f547c-9d7e-48c2-8022-49fb896f0b20">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="192.168.1.7" uuid="d432891a-8082-4b1f-89b4-c55af2fe67de">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://192.168.1.7:3306</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,2 @@
#n:information_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:mysql
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:performance_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:sys
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:information_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:mysql
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:performance_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,2 @@
#n:sys
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,50 @@
<DataSourcesHistory>
<DataSourceFromHistory isRemovedFromProject="false">
<data-source source="LOCAL" name="@localhost" uuid="0a5f547c-9d7e-48c2-8022-49fb896f0b20">
<database-info product="MySQL" version="8.4.9" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-j-9.5.0 (Revision: a7b3c94f50efbddb9f0dd69b3e0d1aaa25305cd6)" dbms="MYSQL" exact-version="8.4.9" exact-driver-version="9.5">
<extra-name-characters>$</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="lower" />
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<secret-storage>master_key</secret-storage>
<user-name>root</user-name>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</DataSourceFromHistory>
<DataSourceFromHistory isRemovedFromProject="false">
<data-source source="LOCAL" name="192.168.1.7" uuid="d432891a-8082-4b1f-89b4-c55af2fe67de">
<database-info product="MySQL" version="8.0.31" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-j-9.5.0 (Revision: a7b3c94f50efbddb9f0dd69b3e0d1aaa25305cd6)" dbms="MYSQL" exact-version="8.0.31" exact-driver-version="9.5">
<extra-name-characters>$</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="exact" quoted-identifiers="exact" />
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://192.168.1.7:3306</jdbc-url>
<secret-storage>master_key</secret-storage>
<user-name>root</user-name>
<schema-mapping>
<introspection-scope>
<node kind="schema">
<name qname="@" />
<name qname="blog" />
<name qname="food_hub" />
<name qname="sweet_hut" />
<name qname="xxl_job" />
</node>
</introspection-scope>
</schema-mapping>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</DataSourceFromHistory>
</DataSourcesHistory>

10
.idea/db-forest-config.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="db-forest-configuration">
<data version="2">.
----------------------------------------
1:0:0a5f547c-9d7e-48c2-8022-49fb896f0b20
2:0:d432891a-8082-4b1f-89b4-c55af2fe67de
.</data>
</component>
</project>

View File

@@ -30,16 +30,15 @@ export default withMermaid({
...routers
],
sidebar: {
'/Web-Front/': [routers[0]],
'/Web-Backend/': [routers[1]],
'/DevOps/': [routers[2]],
'/Flutter/': [routers[3]],
'/Others/': [routers[4]]
'/Web/': [routers[0]],
'/DevOps/': [routers[1]],
'/Practice/': [routers[2]],
'/Others/': [routers[3]]
},
// socialLinks: [
// { icon: 'github', link: 'https://github.com/vuejs/vitepress' }
// ]
socialLinks: [
{ icon: 'gitea', link: 'https://cxxgit.iepose.cn/Cxx0822/blog-press' }
],
footer: {
message: '基于 VitePress 构建',

View File

@@ -34,12 +34,15 @@ import { useData } from 'vitepress'
const wordCount = ref(0)
const imageCount = ref(0)
// 获取页面数据
const { page } = useData()
// 文字阅读时间
const wordTime = computed(() => {
return ((wordCount.value / 275) * 60)
})
// 图片阅读时间
const imageTime = computed(() => {
const n = imageCount.value
if (imageCount.value <= 10) {
@@ -54,13 +57,14 @@ const readTime = computed(() => {
return Math.ceil((wordTime.value + imageTime.value) / 60)
})
function analyze() {
document.querySelectorAll('.meta-des').forEach(v => v.remove())
const analyze = () => {
// 选择文档内容区域
const docDomContainer = window.document.querySelector('#VPContent')
const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>(
'.content-container .main img'
)
// 统计图片数量
const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>('.content-container .main img')
imageCount.value = imgs?.length || 0
// 统计文字数量textContent提取纯文本
const words = docDomContainer?.querySelector('.content-container .main')?.textContent || ''
wordCount.value = countWord(words)
}

View File

@@ -0,0 +1,151 @@
<template>
<Transition name="fade">
<div class="back-top-container" v-show="showBackTop">
<svg class="progress-ring" viewBox="0 0 100 100">
<circle class="progress-ring-background" cx="50" cy="50" r="42" />
<circle
class="progress-ring-circle"
cx="50"
cy="50"
r="42"
:style="{'stroke-dashoffset': circumference - (scrollProgress * circumference)}"
/>
</svg>
<div
class="vitepress-backTop-main"
title="返回顶部"
@click="scrollToTop()"
>
<svg class="icon" viewBox="0 0 1024 1024">
<path d="M752.736 431.063C757.159 140.575 520.41 8.97 504.518 0.41V0l-0.45 0.205-0.41-0.205v0.41c-15.934 8.56-252.723 140.165-248.259 430.653-48.21 31.457-98.713 87.368-90.685 184.074 8.028 96.666 101.007 160.768 136.601 157.287 35.595-3.482 25.232-30.31 25.232-30.31l12.206-50.095s52.47 80.569 69.304 80.528c15.114-1.23 87-0.123 95.6 0h0.82c8.602-0.123 80.486-1.23 95.6 0 16.794 0 69.305-80.528 69.305-80.528l12.165 50.094s-10.322 26.83 25.272 30.31c35.595 3.482 128.574-60.62 136.602-157.286 8.028-96.665-42.475-152.617-90.685-184.074z m-248.669-4.26c-6.758-0.123-94.781-3.359-102.891-107.192 2.95-98.714 95.97-107.438 102.891-107.93 6.964 0.492 99.943 9.216 102.892 107.93-8.11 103.833-96.174 107.07-102.892 107.192z m-52.019 500.531c0 11.838-9.42 21.382-21.012 21.382a21.217 21.217 0 0 1-21.054-21.34V821.74c0-11.797 9.421-21.382 21.054-21.382 11.591 0 21.012 9.585 21.012 21.382v105.635z m77.333 57.222a21.504 21.504 0 0 1-21.34 21.626 21.504 21.504 0 0 1-21.34-21.626V827.474c0-11.96 9.543-21.668 21.299-21.668 11.796 0 21.38 9.708 21.38 21.668v157.082z m71.147-82.043c0 11.796-9.42 21.34-21.053 21.34a21.217 21.217 0 0 1-21.013-21.34v-75.367c0-11.755 9.421-21.299 21.013-21.299 11.632 0 21.053 9.544 21.053 21.3v75.366z" fill="#FFF"/>
</svg>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, computed } from "vue";
const showBackTop = ref(false); // 初始状态设为false
const scrollProgress = ref(0);
// 圆形进度条计算
const radius = 42;
const circumference = computed(() => 2 * Math.PI * radius);
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
// 使用更高效的节流函数
const throttle = (fn, delay = 50) => {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
const updateScrollProgress = () => {
const { scrollY, innerHeight } = window;
const { scrollHeight } = document.documentElement;
const totalScroll = scrollHeight - innerHeight;
scrollProgress.value = totalScroll > 0 ? Math.min(scrollY / totalScroll, 1) : 0;
};
const handleScroll = throttle(() => {
// 当滚动超过100px时显示否则隐藏
const shouldShow = window.scrollY > 100;
showBackTop.value = shouldShow;
updateScrollProgress();
});
onMounted(() => {
window.addEventListener("scroll", handleScroll);
updateScrollProgress();
});
onBeforeUnmount(() => {
window.removeEventListener("scroll", handleScroll);
});
</script>
<style scoped>
.back-top-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
z-index: 999;
}
.vitepress-backTop-main {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
width: 44px;
height: 44px;
border-radius: 50%;
background-color: #3eaf7c;
padding: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
transition: background-color 0.2s ease;
}
.vitepress-backTop-main:hover {
background-color: #71cda3;
}
.progress-ring {
position: absolute;
width: 100%;
height: 100%;
transform: rotate(-90deg);
z-index: 1;
}
.progress-ring-background {
fill: none;
stroke: rgba(62, 175, 124, 0.15);
stroke-width: 3;
}
.progress-ring-circle {
fill: none;
stroke: #3eaf7c;
stroke-width: 3;
stroke-dasharray: 264; /* 2 * π * 42 */
stroke-linecap: round;
transition: stroke-dashoffset 0.15s ease-out;
}
.icon {
width: 24px;
height: 24px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -12,6 +12,10 @@
<template #doc-before>
<ArticleMetadat />
</template>
<template #doc-footer-before>
<Backtotop />
</template>
</DefaultTheme.Layout>
</div>
</Transition>
@@ -21,6 +25,7 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import ArticleMetadat from './ArticleMetadata.vue';
import Backtotop from './Backtotop.vue';
import Loading from './Loading.vue';
import { ref, onMounted } from 'vue'

View File

@@ -4,36 +4,29 @@ import ArticleMetadata from "./components/ArticleMetadata.vue"
import StatsChart from './components/StatsChart.vue'
import MenuList from './components/MenuList.vue'
import type { EnhanceAppContext } from 'vitepress'
import { onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vitepress'
import mediumZoom from 'medium-zoom'
import imageViewer from "vitepress-plugin-image-viewer";
import "viewerjs/dist/viewer.min.css";
import '../theme/style.css'
import './style/var.css'
import './style/vp-code.css'
import './style/custom-block.css'
import MyLayout from './components/MyLayout.vue';
import VPSwiper from '@cssnr/vitepress-swiper'
import '@cssnr/vitepress-swiper/style.css'
export default {
extends: DefaultTheme,
Layout: MyLayout,
setup() {
const route = useRoute()
const initZoom = () => {
// 为所有图片增加缩放功能
mediumZoom('.main img', { background: 'var(--vp-c-bg)' })
}
onMounted(() => {
initZoom()
})
watch(
() => route.path,
() => nextTick(() => initZoom())
)
const route = useRoute();
imageViewer(route);
},
enhanceApp({ app }: EnhanceAppContext) {
app.component("Confetti", Confetti);
app.component("StatsChart", StatsChart);
app.component("MenuList", MenuList);
app.component("ArticleMetadata", ArticleMetadata);
app.component("VPSwiper", VPSwiper);
},
};

View File

@@ -1,82 +1,142 @@
export const routers = [
{
text: '🌐 Web前端',
text: '🌐 Web平台',
items: [
{
text: '🌿 Vue',
items: [
{ text: 'Vue3-Common', link: '/Web-Front/Vue/Vue3-Common' },
{ text: 'Vue3架构原理', link: '/Web-Front/Vue/Vue3-Architecture' }
{ text: 'Vue3架构原理', link: '/Web/Vue/Vue3-Architecture' },
{ text: 'Vue3自定义指令', link: '/Web/Vue/Vue3-Directive' },
{ text: 'Vue3 defineModel', link: '/Web/Vue/Vue3-DefineModel' },
{ text: 'JavaScript知识点整理', link: '/Web/Vue/JavaScript-Guide' },
{ text: '前端包管理器', link: '/Web/Vue/Package' },
{ text: 'Vite核心原理', link: '/Web/Vue/Vite-Principle' },
{ text: 'VueUse简介和使用', link: '/Web/Vue/VueUse' },
{ text: '浏览器渲染全流程解析', link: '/Web/Vue/Browser-Process' },
{ text: 'Element Plus 上传器', link: '/Web/Vue/ElUpload' },
{ text: 'Element Plus 表格组件', link: '/Web/Vue/ElTable' },
]
},
{
text: '🔄 其他',
items: [
{ text: 'JavaScript知识点整理', link: '/Web-Front/Others/JavaScript-Guide' },
{ text: '前端包管理器', link: '/Web-Front/Others/Package' },
{ text: 'Vite核心原理', link: '/Web-Front/Others/Vite-Principle' },
{ text: '浏览器渲染全流程解析', link: '/Web-Front/Others/Browser-Process' }
]
},
]
},
{
text: '🖥️ Web后端',
items: [
{
text: '🍃 SpringBoot',
items: [
{ text: 'Spring IOC简介', link: '/Web-Backend/SpringBoot/SpringIOC' },
{ text: 'SpringBoot启动流程', link: '/Web-Backend/SpringBoot/SpringBoot-Start-Process' },
{ text: 'Spring MVC简介', link: '/Web-Backend/SpringBoot/SpringMVC' },
{ text: 'SpringBoot Starter原理', link: '/Web-Backend/SpringBoot/SpringBootStarter' },
{ text: 'SpringBoot Bean简介', link: '/Web-Backend/SpringBoot/SpringBootBean' },
{ text: 'SpringBoot3原生镜像', link: '/Web-Backend/SpringBoot/SpringBoot3-GraalVM' },
{ text: 'SpingBoot技巧', link: '/Web-Backend/SpringBoot/SpingBoot-Skills' },
{ text: 'SpringBoot Common', link: '/Web-Backend/SpringBoot/SpringBoot-Common' }
{ text: 'Spring IOC简介', link: '/Web/SpringBoot/Spring-IOC' },
{ text: 'Spring AOP简介', link: '/Web/SpringBoot/Spring-AOP' },
{ text: 'Spring MVC简介', link: '/Web/SpringBoot/Spring-MVC' },
{ text: '启动流程', link: '/Web/SpringBoot/SpringBoot-Start-Process' },
{ text: 'Starter原理', link: '/Web/SpringBoot/SpringBoot-Starter' },
{ text: 'Bean简介', link: '/Web/SpringBoot/SpringBoot-Bean' },
{ text: 'SpingBoot注解', link: '/Web/SpringBoot/SpringBoot-Annotation' },
{ text: 'SpringBoot3原生镜像', link: '/Web/SpringBoot/SpringBoot3-GraalVM' },
{ text: 'SpringBoot技巧', link: '/Web/SpringBoot/SpringBoot-Skills' },
{ text: 'RestClient简介', link: '/Web/SpringBoot/SpringBoot-RestClient' },
{ text: 'Redis简介和使用', link: '/Web/SpringBoot/SpringBoot-Redis' },
{ text: 'RocketMQ简介和使用', link: '/Web/SpringBoot/SpringBoot-RocketMQ' },
{ text: 'WebSocket简介和使用', link: '/Web/SpringBoot/SpringBoot-WebSocket' },
{ text: 'Quarzt简介和使用', link: '/Web/SpringBoot/SpringBoot-Quartz' },
{ text: '线程与事件', link: '/Web/SpringBoot/SpringBoot-Thread' }
]
},
{
text: '🐍 FastAPI',
items: [
{ text: 'FastAPI基础教程', link: '/Web-Backend/FastAPI/FastAPI-Guide' },
{ text: '基于OAuth2的安全验证', link: '/Web-Backend/FastAPI/OAuth2' }
{ text: 'FastAPI基础教程', link: '/Web/FastAPI/FastAPI-Guide' },
{ text: '基于OAuth2的安全验证', link: '/Web/FastAPI/OAuth2' },
{ text: 'FastAPI Docker部署', link: '/Web/FastAPI/FastAPI-Docker' }
]
},
{
text: '🐬 MySQL',
items: [
{ text: 'MyBatis简介和使用', link: '/Web/MySQL/MyBatis' },
{ text: 'Flyway简单使用', link: '/Web/MySQL/Flyway' },
{ text: 'MySQL知识点', link: '/Web/MySQL/Summary' },
{ text: 'SQL高阶用法', link: '/Web/MySQL/SQL-Advance' }
]
},
{
text: '🤖 AI',
items: [
{ text: 'Agent开发', link: '/Web/AI/Langchain' },
{ text: 'Text To SQL开发', link: '/Web/AI/TextToSQL' },
{ text: 'RAG开发', link: '/Web/AI/RAG' },
{ text: 'Tools开发', link: '/Web/AI/Tools' },
]
},
{
text: '📶 Network',
items: [
{ text: '网络编程简介', link: '/Web/Network/Summary' },
{ text: '.Net Core实现', link: '/Web/Network/NetCore' },
{ text: 'Mqtt简介与使用', link: '/Web/Network/Mqtt' }
]
},
{
text: '📱 Flutter',
items: [
{ text: 'Flutter简介与安装', link: '/Web/Flutter/FlutterInstall' },
{ text: 'Dart基础教程', link: '/Web/Flutter/DartGuide' },
{ text: 'Flutter基础教程', link: '/Web/Flutter/FlutterGuide' },
{ text: '实战技巧', link: '/Web/Flutter/Practice' }
]
},
{
text: '🔄 其他',
items: [
{ text: 'MySQL知识点', link: '/Web-Backend/Others/MySQL-Knowledge' },
{ text: 'Flyway简单使用', link: '/Web-Backend/Others/Flyway' },
{ text: 'MyBatis简介和使用', link: '/Web-Backend/Others/MyBatis' },
{ text: 'RustFS简介和使用', link: '/Web-Backend/Others/RustFS' },
{ text: 'AList简介和使用', link: '/Web-Backend/Others/AList' },
{ text: '网络编程简介', link: '/Web-Backend/Others/Netword-Program' }
{ text: 'RustFS简介和使用', link: '/Web/Others/RustFS' },
{ text: 'AList简介和使用', link: '/Web/Others/AList' },
{ text: 'InfluxDB简介和使用', link: '/Web/Others/InfluxDB' },
{ text: 'Meilisearch简介和使用', link: '/Web/Others/Meilisearch' },
{ text: '视频压缩', link: '/Web/Others/VideoCompressor' },
{ text: '图片处理服务', link: '/Web/Others/ImageProcessor' },
{ text: '秒杀系统设计与实战', link: '/Web/Others/Seckill' }
]
},
]
},
{
text: '🚀 运维部署',
items: [
{ text: '⭐ OpenObserve部署和使用', link: '/DevOps/OpenObserve' },
{ text: 'Jenkins部署和使用', link: '/DevOps/Jenkins' },
{ text: 'ElasticSearch部署和使用', link: '/DevOps/ElasticSearch' },
{ text: 'Docker简介和安装', link: '/DevOps/Docker' },
{ text: 'Linux命令', link: '/DevOps/LinuxCommand' }
]
},
{
text: '🚀 DevOps',
items: [
{ text: 'OpenObserve部署和使用', link: '/DevOps/OpenObserve' },
{ text: 'Jenkins部署和使用', link: '/DevOps/Jenkins' },
{ text: 'ElasticSearch部署和使用', link: '/DevOps/ElasticSearch' },
{ text: 'Docker简介和安装', link: '/DevOps/Docker' }
]
},
{
text: '📱 Flutter',
text: '🏗️ 项目实战',
items: [
{ text: 'Dart基础教程', link: '/Flutter/DartGuide' },
{ text: 'Flutter基础教程', link: '/Flutter/FlutterGuide' },
{ text: '实战技巧', link: '/Flutter/Practice' }
]
{
text: '🧩 通用模块',
items: [
{ text: 'Vue3 Common', link: '/Practice/Common/Vue3-Common' },
{ text: 'SpringBoot Common', link: '/Practice/Common/SpringBoot-Common' },
]
},
{
text: '🏢 公司项目',
items: [
{ text: '埃学习', link: '/Practice/Company/Learn' }
]
},
{
text: '👤 个人项目',
items: [
{ text: '家庭管理系统', link: '/Practice/Personal/SweetHut' },
{ text: '个人博客系统', link: '/Practice/Personal/Blog' }
]
},
]
},
{
text: '🔄 其他',
items: [
{ text: 'VitePress使用和简介', link: '/Others/VitePress' },
{ text: '企业微信工作台开发', link: '/Others/WeCom' },
{ text: 'XDRP简介和安装', link: '/Others/XDRP' },
{ text: 'RustDesk简介和安装', link: '/Others/RustDesk' },
{ text: 'Systemback制作Ubuntu镜像', link: '/Others/Systemback' },
]
}
];

View File

@@ -36,15 +36,10 @@
border-top: none;
}
.medium-zoom-overlay {
background-color: var(--vp-c-bg) !important;
z-index: 100;
.vp-swiper-button {
color: #bd34fe;
}
.medium-zoom-overlay ~ img {
z-index: 101;
}
.medium-zoom--opened .medium-zoom-overlay {
opacity: 0.9 !important;
strong {
color: #bd34fe;
}

View File

@@ -1,7 +1,6 @@
---
title: Docker简介和安装
date: 2025-12-15
isGreat: true
---
# 一、Docker简介
@@ -288,7 +287,7 @@ docker system prune -f
# 六、Dockerfile
&emsp;&emsp;Dockerfile 是用来构建 Docker 镜像的文本文件,包含了一系列构建指令,可以自动化地创建包含应用及其运行环境的镜像。
&emsp;&emsp;基本结构:
```
```dockerfile
# 基础镜像
FROM 镜像名:标签
@@ -344,18 +343,13 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"]
```
```dockerfile [dotnet]
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY publish/ .
RUN chmod +x EMMessageHubServer
EXPOSE 5000
COPY ./publish .
EXPOSE 14040
ENTRYPOINT ["./EMMessageHubServer"]
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
ENTRYPOINT ["dotnet", "App.dll"]
```
:::

100
docs/DevOps/LinuxCommand.md Normal file
View File

@@ -0,0 +1,100 @@
---
title: Linux命令
date: 2026-05-09
---
# 一、系统信息查看
| 命令 | 说明 | 示例 | 参数/注释 |
| :---: | :---: | :---: | :---: |
| uname | 查看系统内核信息 | uname | -a:所有信息 |
| lsb_release -a | 查看发行版信息 | - | - |
| df | 查看磁盘空间使用 | df -h | -h:显示可读的文件大小 |
| du | 计算文件/目录大小 | du -sh * | - |
| free | 查看内存使用 | free -h | - |
| uptime | 查看系统运行时间 | uptime | - |
::: tip
查看指定目录下每个文件/文件夹的大小,并按大小排序:
```bash
du -sh /root/* | sort -hr
```
:::
# 二、文件和目录操作
| 命令 | 说明 | 示例 | 参数/注释 |
| :---: | :---: | :---: | :---: |
| ls | 列出目录内容 | ls /home -lah | -a:显示隐藏文件 <br> -l:显示详细信息 <br> -h:显示可读的文件大小 |
| pwd | 显示当前路径 | - | - |
| cp | 复制文件或目录 | cp -r src/ dst/ | -r:文件夹 |
| mv | 移动或重命名 | mv file.txt new.txt | 建议写完整路径 |
| find | 查找文件 | find . -name "*.txt" <br> find . -type f -size +10M | - |
# 三、文件内容查看
| 命令 | 说明 | 示例 | 参数/注释 |
| :---: | :---: | :---: | :---: |
| cat | 查看整个文件 | cat /etc/os-release | - |
| less | 分页浏览(支持上下翻) | less /var/log/syslog | q:退出 <br> ↑↓:上下一行 <br> b/空格:上下一屏|
| more | 分页浏览(仅向下) | more /var/log/syslog | q:退出 <br> 空格:下一屏|
| head | 查看文件开头 N 行 | head /var/log/syslog | -n 20:前20行 |
| tail | 查看文件末尾 N 行 | tail /var/log/syslog | -n 20:后20行 <br> -f:实时显示|
# 四、进程管理
| 命令 | 说明 | 示例 | 参数/注释 |
| :---: | :---: | :---: | :---: |
| ps | 查看当前进程快照 | ps aux | - |
| top | 实时进程监控 | top | 实时刷新按q退出 <br> P(大写):CPU排序 <br> M:内存排序 |
| kill | 终止进程 | kill PID | -9:强制关闭 |
| pkill | 按名称终止进程 | pkill PID | -9:强制关闭 |
| nohup | 后台运行且忽略挂断 | nohup python app.py & | - |
::: tip
1. 查看某个程序进程:
```bash
ps aux | grep nginx
```
2. 查看指定 PID 的进程:
```bash
ps -fp PID号
```
3. 按 CPU 排序
```bash
ps aux --sort=-%cpu | head -20
```
4. 按内存排序
```bash
ps aux --sort=-%mem | head -20
```
5. nohup输出日志
```bash
nohup command > app.log 2>&1 &
```
6. nohup不输出日志
```bash
nohup command > /dev/null 2>&1 &
```
:::
# 五、网络管理
| 命令 | 说明 | 示例 | 参数/注释 |
| :---: | :---: | :---: | :---: |
| ip | 查看/配置网络接口 | - | link:查看所有网卡 <br> addr:查看 IP 地址 <br> route:查看路由表 |
| ss | 查看网络连接、端口监听、套接字统计 | - | -l监听 <br> -n不解析服务名 <br> -tTCP <br> -p显示进程 PID / 程序名 |
::: tip
1. 显示进程端口
```bash
ss -lntp
```
2. 查看某个端口是否启动
```bash
ss -lntp | grep 8080
```
:::
# 六、实战场景

View File

@@ -1,6 +1,7 @@
---
title: OpenObserve部署和使用
date: 2025-11-26
isGreat: true
---
# 一、基础概念
@@ -9,7 +10,7 @@
### 1.1.1 特点
1. 极致的轻量级​
Fluent Bit 是用 C 语言编写的,运行时内存占用极低(通常只有几百 KBCPU 使用率也非常低。这使得它成为在资源受限环境如边缘计算、IoT 设备、容器侧车)中运行的理想选择。
Fluent Bit 是用 C 语言编写的,运行时内存占用极低(通常只有几百 KBCPU 使用率也非常低。这使得它成为在资源受限环境如边缘计算、IoT 设备、容器)中运行的理想选择。
2. 高性能​
其核心架构经过高度优化,处理数据的速度极快,能够轻松应对高吞吐量的日志流。
3. 可插拔架构​
@@ -23,7 +24,17 @@ Fluent Bit 的功能通过“插件”来扩展。
3. Output输出
定义数据目的地,例如 Elasticsearch、Splunk、Kafka、Datadog、Loki、S3、另一个 Fluentd/Fluent Bit 实例等。
### 1.1.3 核心架构与数据流
::: tip
常用Input插件
1. Forward
&emsp;&emsp;适用于Fluent Bit / Fluentd 节点,传输协议为私有二进制协议。支持压缩和重连,传输效率极高,但客户端必须使用特定的库。
2. TCP
&emsp;&emsp;适用于能建立 Socket 连接的程序传输协议为TCP协议只负责接收字节流通常需要配合解析器Parser来处理格式。
3. HTTP
&emsp;&emsp;通过标准的 HTTP POST 请求接收 JSON 数据传输协议为HTTP协议兼容性最好任何编程语言都能轻松发送 HTTP 请求。
:::
### 1.1.3 核心架构
1. Input输入
一个或多个输入插件持续地从数据源收集数据。每条数据被读入后,会带上时间戳和标签等信息,被包装成一个内部的事件记录。
2. Parser解析器
@@ -37,6 +48,20 @@ Fluent Bit 的功能通过“插件”来扩展。
6. Output输出
最终,输出插件将处理好的数据发送到配置的目的地。
### 1.1.4 安装
```bash
sudo sh -c 'curl https://packages.fluentbit.io/fluentbit.key | gpg --dearmor > /usr/share/keyrings/fluentbit-keyring.gpg'
CODENAME=$(lsb_release -cs) && echo "deb [signed-by=/usr/share/keyrings/fluentbit-keyring.gpg] https://packages.fluentbit.io/ubuntu/$CODENAME $CODENAME main" | sudo tee /etc/apt/sources.list.d/fluent-bit.list
apt-get update
apt-get install fluent-bit
systemctl start fluent-bit.service
systemctl enable fluent-bit.service
```
::: tip
默认安装位置为:/opt/fluent-bit/bin/fluent-bit默认配置文件位置为/etc/fluent-bit。
:::
## 1.2 OpenObserve
&emsp;&emsp;[OpenObserve](https://openobserve.ai/) 是一个开源的、云原生的可观测性数据平台,旨在处理日志、指标、追踪和性能数据。它的目标是成为 Elasticsearch/Logstash/Kibana (ELK)Grafana LokiDatadog 等流行解决方案的​​更简单、更经济高效、更高性能​​的替代品。
&emsp;&emsp;OpenObserve 是一个“一体化”的平台,你可以将所有的可观测性数据(日志、指标、追踪)都发送到它这里,然后通过统一的界面进行搜索、分析和可视化。
@@ -65,7 +90,7 @@ services:
- ./openobserve-data:/data
environment:
- ZO_ROOT_USER_EMAIL=1556464090@qq.com
- ZO_ROOT_USER_PASSWORD=19940822Cxx
- ZO_ROOT_USER_PASSWORD=yourpassword
- TZ=Asia/Shanghai
ports:
- "5080:5080"
@@ -81,7 +106,7 @@ volumes:
driver: local
```
&emsp;&emsp;fluent bit配置文件
&emsp;&emsp;fluent bit配置文件fluent-bit.conf
```conf
[SERVICE]
Flush 5
@@ -102,16 +127,466 @@ volumes:
Port 5080
URI /api/default/default/_json
http_user 1556464090@qq.com
http_passwd 19940822Cxx
http_passwd yourpassword
Format json
json_date_key timestamp
json_date_format iso8601
```
&emsp;&emsp;INPUT为日志输入源监听24422端口。
&emsp;&emsp;OUTPUT为日志输出源将日志发送到openobserve中这里使用的是容器名称URI表示openobserve接收数据的api路径default/default表示组织名称/流名称可以更改为其他的名称。_json表示以json的格式。
&emsp;&emsp;INPUT为日志输入源使用Forward插件监听24422端口。
&emsp;&emsp;OUTPUT为日志输出源将日志发送到openobserve中这里使用的是容器名称URI表示openobserve接收数据的api路径default/default表示组织名称/流名称可以更改为其他的名称。_json表示以json的格式。
&emsp;&emsp;Match *表示接收所有的INPUT也可以根据INPUT的tag来区分分发到哪个OUTPUT。
# 三、简单使用
# 三、数据接入
## 3.1 SpringBoot
### 3.1.1 安装依赖
```xml
<properties>
<logback-more-appenders.version>1.8.8</logback-more-appenders.version>
<fluency-fluentd.version>2.7.0</fluency-fluentd.version>
</properties>
<dependencies>
<!-- SpringBoot 日志启动 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 扩展 logback appender -->
<dependency>
<groupId>com.sndyuk</groupId>
<artifactId>logback-more-appenders</artifactId>
<version>${logback-more-appenders.version}</version>
</dependency>
<!-- Fluentd 日志搜集和转发 -->
<dependency>
<groupId>org.komamitsu</groupId>
<artifactId>fluency-fluentd</artifactId>
<version>${fluency-fluentd.version}</version>
</dependency>
</dependencies>
```
### 3.1.2 日志配置
```yml
logging:
config: classpath:logback-spring.xml
fluentd:
host: host.docker.internal
port: 24224
level:
root: info
```
### 3.1.3 配置文件
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 自定义属性 -->
<springProperty scope="context" name="fluentdHost" source="logging.fluentd.host" defaultValue="localhost"/>
<springProperty scope="context" name="fluentdPort" source="logging.fluentd.port" defaultValue="24224"/>
<springProperty scope="context" name="topic" source="spring.application.name" defaultValue="default"/>
<!-- log路径 -->
<property name="log.path" value="logs/${topic}"/>
<!--控制台日志格式:彩色日志-->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
<!--文件日志格式-->
<property name="FILE_LOG_PATTERN"
value="%date{yyyy-MM-dd HH:mm:ss} |%-5level |%thread |%file:%line |%logger |%msg%n"/>
<!--编码-->
<property name="ENCODING"
value="UTF-8"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--日志级别-->
<level>DEBUG</level>
</filter>
<encoder>
<!--日志格式-->
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!--日志字符集-->
<charset>${ENCODING}</charset>
</encoder>
</appender>
<!--输出到文件-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志过滤器此日志文件只记录INFO级别的-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志文件的最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志过滤器此日志文件只记录WARN级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志文件的最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志过滤器此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志文件的最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<!--输出到FLUENCY-->
<appender name="FLUENCY" class="ch.qos.logback.more.appenders.FluencyLogbackAppender">
<remoteHost>${fluentdHost}</remoteHost>
<port>${fluentdPort}</port>
<additionalField>
<key>topic</key>
<value>${topic}</value>
</additionalField>
<ignoredField>caller</ignoredField>
<useEventTime>true</useEventTime>
</appender>
<!-- 开发环境 -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 生产环境 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- 容器环境 -->
<springProfile name="docker">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FLUENCY"/>
</root>
</springProfile>
</configuration>
```
::: tip
这里是用docker部署的如果是非docker部署host为服务器ip地址。
:::
## 3.2 .NET Core
### 3.2.1 安装Nuget包
&emsp;&emsp;安装NLog.Extensions.Hosting和NLog.Targets.Network包。
::: tip
NLog.Targets.Fluentd这个包也可以但是无法定制FluentBit的消息格式。
:::
### 3.2.2 使用NLog
```cs
var host = Host.CreateDefaultBuilder(args)
.UseNLog()
.Build();
```
### 3.2.3 配置NLog
&emsp;&emsp;在项目目录创建`NLog.config`并设置复制到输出目录为始终复制。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true">
<extensions>
<add assembly="NLog.Targets.Network" />
</extensions>
<targets async="true">
<target name="fluentbit" xsi:type="Network"
newLine="true" lineEnding="LF"
address="tcp://ip:5170">
<layout xsi:type="JsonLayout">
<attribute name="topic" layout="message-hub"/>
<attribute name="timestamp" layout="${longdate:universalTime=true}"/>
<attribute name="level" layout="${level:uppercase=true}"/>
<attribute name="logger" layout="${logger}"/>
<attribute name="message" layout="${message}"/>
<attribute name="throwable" layout="${exception:format=ToString}"/>
</layout>
</target>
<target name="stdout"
xsi:type="Console"
layout="${longdate} ${level:uppercase=true}: ${logger} - ${message}${onexception:${newline}${exception:format=ToString}}" />
</targets>
<rules>
<logger name="*" minlevel="Info" writeTo="stdout, fluentbit"/>
</rules>
</nlog>
```
::: tip
这里的topic为项目名称。
:::
### 3.2.3 配置插件
&emsp;&emsp;在FluentBit中增加TCP插件
```conf
[INPUT]
Name tcp
Listen 0.0.0.0
Port 5170
Tag tcp
```
::: tip
docker-compose文件中也要增加5170端口映射。
:::
## 3.3 Python
### 3.3.1 安装依赖
```bash
pip install fluent-logger
```
### 3.3.2 配置文件
```.env
FLUENT_HOST=ip
FLUENT_PORT=24224
FLUENT_TAG=test-server
```
### 3.3.3 日志配置
```py
import logging
from datetime import datetime
from fluent import sender, event
from dotenv import load_dotenv
import os
load_dotenv()
# Fluent-bit配置
FLUENT_HOST = os.getenv("FLUENT_HOST", "localhost")
FLUENT_PORT = int(os.getenv("FLUENT_PORT", 24224))
FLUENT_TAG = os.getenv("FLUENT_TAG", "default")
class FluentHandler(logging.Handler):
"""Fluent-bit日志处理器"""
def emit(self, record):
try:
log_data = {
"level": record.levelname,
"message": record.message,
"module": record.module,
"line": record.lineno,
"topic": FLUENT_TAG,
}
event.Event(record.levelname.lower(), log_data)
except Exception as e:
print(f"Fluent日志发送失败: {str(e)}")
def setup_logger():
logger = logging.getLogger("app_logger")
logger.setLevel(logging.DEBUG)
logger.propagate = False # 防止日志重复输出
# 日志格式
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s")
# 控制台处理器(所有环境启用)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setLevel(logging.DEBUG)
logger.addHandler(console_handler)
# 输出到fluent-bit
sender.setup(FLUENT_TAG, host=FLUENT_HOST, port=FLUENT_PORT)
fluent_handler = FluentHandler()
fluent_handler.setFormatter(formatter)
fluent_handler.setLevel(logging.INFO)
logger.addHandler(fluent_handler)
return logger
# 全局日志实例
logger = setup_logger()
```
## 3.4 Unity
### 3.4.1 日志配置
```cs
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System;
public class FluentBitLogger : MonoBehaviour
{
private string url = "http://ip:9880";
void OnEnable() => Application.logMessageReceived += HandleLog;
void OnDisable() => Application.logMessageReceived -= HandleLog;
void HandleLog(string logString, string stackTrace, LogType type)
{
// 1. 准备数据
var logData = new LogData
{
topic = "default",
message = logString,
logType = type.ToString(),
timestamp = DateTime.Now.ToString("o")
};
// 2. 转 JSON 并发送
string json = JsonUtility.ToJson(logData);
StartCoroutine(Send(json));
}
IEnumerator Send(string json)
{
// 快速判断设备是否连接到互联网
if (Application.internetReachability == NetworkReachability.NotReachable)
{
yield break;
}
using (UnityWebRequest req = UnityWebRequest.Put(url, json))
{
req.method = "POST";
req.timeout = 1;
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
}
}
[Serializable]
public class LogData
{
public string topic;
public string message;
public string logType;
public string timestamp;
}
}
```
### 3.4.2 配置插件
&emsp;&emsp;在FluentBit中增加http插件
```conf
[INPUT]
Name http
Listen 0.0.0.0
Port 9880
Tag unity.logs
```
::: tip
docker-compose文件中也要增加9880端口映射。
:::
## 3.5 Vue
&emsp;&emsp;发送HTTP数据到FluentBit中
```ts
export const sendError = (err, instance, info) => {
try {
const log = {
top: 'vue-ui',
level: 'ERROR',
info,
component: instance?.$options?.name || 'Anonymous',
message: err.message,
url: location.href
}
fetch('http://ip:9880', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(log)
})
} catch {
}
}
```
&emsp;&emsp;利用app.config.errorHandler监听异常
```ts
app.config.errorHandler = (err, instance, info) => {
sendError(err, instance, info)
}
```
# 四、简单使用
## 3.1 配置数据流
&emsp;&emsp;打开控制台,选择数据流->添加数据流输入fluent bit配置文件中的流名称选择数据流类型为Logs。
&emsp;&emsp;在新添加的数据流中,点击查看,跳转到日志页面即可看到上次的日志信息。
@@ -130,7 +605,7 @@ openobserve:
- ./openobserve-data:/data
environment:
- ZO_ROOT_USER_EMAIL=njcxx0822@163.com
- ZO_ROOT_USER_PASSWORD=19940822Cxx
- ZO_ROOT_USER_PASSWORD=yourpassword
- TZ=Asia/Shanghai
- ZO_SMTP_ENABLED=true
@@ -221,7 +696,7 @@ curl -X 'POST' \
&emsp;&emsp;注意这里要加上Basic auth认证并提供username和password。
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/d29ddbd369e32cb7c2770464e8d0a142.png 'API查看日志.png')
# 、自定义日志告警服务
# 、自定义日志告警服务
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/74d5efdb80f506395b81988258433d9d.png '日志框架.png')
&emsp;&emsp;基本原理为通过提供一个HTTP的API接口当Openobserver触发告警时调用该接口并传入当前的alert_name和timestamp然后根据timestamp和Openobserver提供的API接口去查询最近一段时间内的所有日志信息并存储到SqLite数据库中。同时也可以选择是否同步推送到企业微信和邮箱。
&emsp;&emsp;最后也需要提供一个查询历史日志告警信息的接口,并可视化显示,方便运维和开发查看。

View File

@@ -1,10 +1,10 @@
---
layout: doc
title: DevOps
title: 运维部署
---
<script setup>
import { routers } from '../.vitepress/theme/router'
</script>
<MenuList :routers=routers[2] />
<MenuList :routers=routers[1] />

108
docs/Others/RustDesk.md Normal file
View File

@@ -0,0 +1,108 @@
---
title: RustDesk简介和安装
date: 2026-03-24
---
# 一、简介
&emsp;&emsp;RustDesk是一款开源的远程桌面控制软件它允许用户通过网络从一台计算机远程控制另一台计算机。
&emsp;&emsp;特点:
1. 跨平台:支持 Windows、macOS、Linux、Android、iOS 等主流操作系统。
2. 自托管:你可以搭建自己的中继服务器,将数据完全掌握在自己手中,保障安全和隐私。
3. 高性能:使用 Rust 语言编写,并采用了先进的视频编解码技术(如 H.264),力求在低带宽下也能实现流畅的远程控制体验。
4. 功能全面:支持文件传输、远程终端、语音通话、多显示器切换、远程重启等常用功能。
# 二、安装
## 2.1 自建服务器安装
&emsp;&emsp;[RustDesk文档-Docker](https://rustdesk.com/docs/zh-cn/self-host/rustdesk-server-oss/docker/)
```yml
services:
hbbs:
container_name: hbbs
image: rustdesk/rustdesk-server:latest
command: hbbs
volumes:
- ./data:/root
network_mode: "host"
depends_on:
- hbbr
restart: unless-stopped
hbbr:
container_name: hbbr
image: rustdesk/rustdesk-server:latest
command: hbbr
volumes:
- ./data:/root
network_mode: "host"
restart: unless-stopped
```
::: tip
需要开放以下端口:
hbbs:
&emsp;&emsp;21114 (TCP): 用于网页控制台,仅在 Pro 版本中可用。
&emsp;&emsp;21115 (TCP): 用于 NAT 类型测试。
&emsp;&emsp;21116 (TCP/UDP): 请注意 21116 应该同时为 TCP 和 UDP 启用。 21116/UDP 用于 ID 注册和心跳服务。21116/TCP 用于 TCP 打洞和连接服务。
&emsp;&emsp;21118 (TCP): 用于支持网页客户端。
hbbr:
&emsp;&emsp;21117 (TCP): 用于中继服务。
&emsp;&emsp;21119 (TCP): 用于支持网页客户端。
:::
## 2.2 客户端安装
&emsp;&emsp;进入[RustDesk官网](https://rustdesk.com/zh-cn/),根据系统类型选择相应的安装包下载并安装。
## 2.3 客户端配置
&emsp;&emsp;例如在Windows系统中打开exe文件可以直接免安装打开也可以安装到本地点击右上角设置选择网络->ID/中继服务器配置ID服务器、中继服务器和Key值。
&emsp;&emsp;其中ID服务器和中继服务器为自建服务器的IP地址Key值可以通过docker日志查看。
# 三、RustDeskApi
&emsp;&emsp;官方RustDesk自建服务的Web Api和管理后台是需要收费的可以使用免费的[RustDeskApi](https://github.com/lejianwen/rustdesk-api)项目。
## 3.1 自建服务器安装
```yml
networks:
rustdesk-net:
driver: bridge
volumes:
rustdesk_server_data:
rustdesk_api_data:
services:
rustdesk-full:
container_name: rustdesk-full
image: lejianwen/rustdesk-api:full-s6
ports:
- "21114:21114" # API 服务端口
- "21115:21115" # hbbs 端口
- "21116:21116" # hbbs 端口
- "21116:21116/udp" # hbbs UDP 端口
- "21117:21117" # hbbr 中继端口
- "21118:21118" # WebSocket 端口Web客户端
- "21119:21119" # 额外中继端口
environment:
- RELAY=<ip>:21117
- ENCRYPTED_ONLY=1
- MUST_LOGIN=N
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=<ip>:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=<ip>:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://<ip>:21114
- RUSTDESK_API_KEY_FILE=/app/conf/data/id_ed25519.pub
volumes:
- ./data/server:/data
- ./data/server:/app/conf/data
- ./data/api:/app/data
networks:
- rustdesk-net
restart: unless-stopped
```
::: tip
这里采用官方S6镜像[Wiki](https://github.com/lejianwen/rustdesk-api/wiki)其中ip为自建服务器ip。
:::
&emsp;&emsp;输入`http://ip:21114/_admin/`进入管理员后台。
&emsp;&emsp;或者输入`http://ip:21114/webclient/`直接进入RustDesk Web客户端。

37
docs/Others/Systemback.md Normal file
View File

@@ -0,0 +1,37 @@
---
title: Systemback制作Ubuntu镜像
date: 2026-03-12
---
# 一、Systemback简介
&emsp;&emsp;Systemback 是一款用于 Ubuntu 等基于 Ubuntu 的 Linux 发行版的系统管理工具。它主要功能是创建系统备份和还原系统,类似于 Windows 系统中的 “系统还原” 功能。
# 二、安装
```bash
sudo sh -c 'echo "deb [arch=amd64] http://mirrors.bwbot.org/ stable main" > /etc/apt/sources.list.d/systemback.list'
sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key 50B2C005A67B264F
sudo apt-get update
sudo apt-get install systemback
```
# 三、制作镜像
1. 打开Systemback工具
```bash
sudo systemback
```
2. 选择 **Live system create**
3. 勾选include the user data files
4. 点击 **Create new**
5. 等待制作
6. 插入U盘在Write target中选择U盘点击 **Write to target**
7. 等待制作完成
# 四、还原镜像
1. 插入U盘进入安装界面选择 **Boot System installer**
2. 设置用户名和密码
3. 设置分区,**注意一定要设置/boot/efi分区**
4. 等待还原
# 五、参考资料
1. [使用systemback备份和还原Ubuntu](https://blog.csdn.net/weixin_51526597/article/details/135712523)
2. [Systemback Ubuntu18.04 制作系统ISO镜像并物理机还原](https://www.zhihu.com/tardis/zm/art/576420693?source_id=1005)

View File

@@ -3,3 +3,554 @@
date: 2025-12-18
---
# 一、简介
&emsp;&emsp;[VitePress](https://vitepress.dev/zh/)是由 Vue 团队开发的静态网站生成器 (SSG),基于 Vite前端构建工具和 Vue 3 构建,专门用于快速搭建文档网站、博客或个人主页。它继承了 Vite 的极速开发体验,同时具备 Vue 的组件化能力,语法上兼容 Markdown 并支持扩展 Vue 组件,是替代 VuePress 的新一代工具。
# 二、快速开始
## 2.1 安装
```sh
pnpm add -D vitepress@next
```
## 2.2 初始化
```sh
pnpm vitepress init
```
::: tip
建议按照官网配置,文档目录设置为`./docs`
:::
## 2.3 运行
```sh
pnpm run docs:dev
```
## 2.4 使用
&emsp;&emsp;`docs`文件夹新建`md`文档,并在`.vitepress/config.mts`中配置`nav``sidebar`路径。
# 三、优化
## 3.1 路由组件
&emsp;&emsp;每次新建文档时,都需要在`.vitepress/config.mts`重复`nav``sidebar`路径,有时还需要在文档目录的`index.md`配置导航链接每次都要重复写3次很麻烦因此可以构造一个路由配置清单和路由组件每次只需更新一次路由配置清单即可。
&emsp;&emsp;`.vitepress/theme/router`文件夹下新建路由清单文件:
```ts
export const routers = [
{
text: '🌐 Web前端',
items: [
{
text: '🌿 Vue',
items: [
{ text: 'Test', link: '/Web-Front/Vue/Test' }
]
},
{
text: '🔄 其他',
items: [
{ text: 'Test', link: '/Web-Front/Others/Test' }
]
},
]
},
{
text: '🖥️ Web后端',
items: [
{
text: '🍃 SpringBoot',
items: [
{ text: 'Test', link: '/Web-Backend/SpringBoot/Test' }
]
},
{
text: '🐍 FastAPI',
items: [
{ text: 'Test', link: '/Web-Backend/FastAPI/Test' }
]
},
{
text: '🔄 其他',
items: [
{ text: 'Test', link: '/Web-Backend/Others/Test' }
]
},
]
},
{
text: '🚀 DevOps',
items: [
{ text: 'Test', link: '/DevOps/Test' }
]
}
];
```
&emsp;&emsp;这里列举了有二级菜单和只有一级菜单的两种情况。
&emsp;&emsp;`.vitepress/theme/components`文件夹下新建路由组件`MenuList.vue`
```vue
<template>
<div v-if="isNested">
<div v-for="category in props.routers.items" :key="category.text">
<h3>{{ category.text }}</h3>
<ul>
<li v-for="item in category.items" :key="item.text">
<a :href="withBase(item.link)">{{ item.text }}</a>
</li>
</ul>
</div>
</div>
<div v-else>
<ul>
<li v-for="item in props.routers.items" :key="item.text">
<a :href="withBase(item.link)">{{ item.text }}</a>
</li>
</ul>
</div>
</template>
<script setup>
import { withBase } from 'vitepress'
import { defineProps } from 'vue'
const props = defineProps({
routers: {
required: true,
type: Object
},
isNested: {
type: Boolean,
default: false
}
})
</script>
```
&emsp;&emsp;这里需要使用`withBase`拼接路由,防止存在设置了`base`根路径的情况。通过`isNested`来控制是否存在二级菜单。
::: warning
这里只考虑了最多存在二级菜单的情况。
:::
&emsp;&emsp;在`.vitepress/theme/index.ts`注册组件:
```ts
import MenuList from './components/MenuList.vue'
import type { EnhanceAppContext } from 'vitepress'
export default {
enhanceApp({ app }: EnhanceAppContext) {
app.component("MenuList", MenuList);
},
};
```
&emsp;&emsp;`.vitepress/config.mts`中配置`nav``sidebar`
```ts
export default defineConfig({
themeConfig: {
nav: [
{ text: '🏠 Home', link: '/' },
...routers
],
sidebar: {
'/Web-Front/': [routers[0]],
'/Web-Backend/': [routers[1]],
'/DevOps/': [routers[2]]
},
},
lastUpdated: true,
})
```
&emsp;&emsp;在文档所在目录的`index.md`中配置:
```md
---
layout: doc
title: Web前端
description: Web前端开发技术文档
---
<script setup>
import { routers } from '../.vitepress/theme/router'
</script>
<MenuList :routers=routers[0] :isNested=true />
```
&emsp;&emsp;这样后续只需要维护路由配置清单文件即可自动生成路由菜单。
## 3.2 博客元数据
&emsp;&emsp;可以通过在每个博客中添加`frontmatter`元数据,在最顶部添加博客属性,例如创建日期、字数、时长和是否精品等信息。
### 3.2.1 定义元数据
&emsp;&emsp;在每个博客最上面添加元数据:
```md
---
title: Docker简介和安装
date: 2025-12-15
isGreat: true
---
```
&emsp;&emsp;这里可以自定义添加任何`key: value`形式的字段信息。
::: tip
建议手动加上每个博客的创建日期
:::
### 3.2.2 博客信息组件
&emsp;&emsp;`.vitepress/theme/components`文件夹下新建博客信息组件`ArticleMetadata.vue`
```vue
<template>
<div class="title">
<span>{{ page.frontmatter.title }}</span>
</div>
<div v-if="!page.filePath.includes('index.md')" class="blog-stats">
<div v-if="page.frontmatter.isGreat" class="item">
</svg>
精品
</div>
<div class="item">
</svg>
日期: {{ page.frontmatter.date.split('T')[0] }}
</div>
<div class="item">
</svg>
字数: {{ formatNumberUnit(wordCount) }}
</div>
<div>
</svg>
时长: {{ readTime }} 分钟
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted, watch } from 'vue'
import { countWord, formatNumberUnit } from '../utils'
import { useData } from 'vitepress'
const wordCount = ref(0)
const imageCount = ref(0)
// 获取页面数据
const { page } = useData()
// 文字阅读时间
const wordTime = computed(() => {
return ((wordCount.value / 275) * 60)
})
// 图片阅读时间
const imageTime = computed(() => {
const n = imageCount.value
if (imageCount.value <= 10) {
// 等差数列求和
return n * 13 + (n * (n - 1)) / 2
}
return 175 + (n - 10) * 3
})
// 阅读时间
const readTime = computed(() => {
return Math.ceil((wordTime.value + imageTime.value) / 60)
})
const analyze = () => {
// 选择文档内容区域
const docDomContainer = window.document.querySelector('#VPContent')
// 统计图片数量
const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>('.content-container .main img')
imageCount.value = imgs?.length || 0
// 统计文字数量textContent提取纯文本
const words = docDomContainer?.querySelector('.content-container .main')?.textContent || ''
wordCount.value = countWord(words)
}
watch(() => page.value.title, () => {
// 路径变化时执行
analyze()
})
onMounted(() => {
// 初始化时执行一次
analyze()
})
</script>
<style scoped>
.blog-stats {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.title {
margin-bottom: 5px;
padding: 10px 0;
text-align: center;
font-size: 32px;
color: #fff;
font-weight: bold;
background: -webkit-linear-gradient(10deg, #3DC8F0 5%, #8036FA 15%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.icon {
display: inline-block;
transform: translate(0px , 2px);
}
</style>
```
&emsp;&emsp;这里统计了正文部分的文字和图片数量,并通过相应规则计算阅读时间。
&emsp;&emsp;通过`page.frontmatter`获取自定义的元数据信息并在博客最上面显示。
### 3.2.3 注册组件
&emsp;&emsp;`.vitepress/theme/index.ts`注册组件参考3.1节内容)。
### 3.2.4 自定义布局组件
&emsp;&emsp;需要将博客信息组件放在自定义布局组件中。
&emsp;&emsp;`.vitepress/theme/components`文件夹下新建自定义布局组件`MyLayout.vue`
```vue
<template>
<DefaultTheme.Layout v-bind="$attrs">
<template #doc-before>
<ArticleMetadat />
</template>
</DefaultTheme.Layout>
</template>
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import ArticleMetadat from './ArticleMetadata.vue';
</script>
```
&emsp;&emsp;更多插槽可以参考[Github源码](https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/Layout.vue)。
&emsp;&emsp;同样需要注册组件。
### 3.2.5 配置布局组件
&emsp;&emsp;`.vitepress/config.mts`中配置布局:
```ts
import DefaultTheme from 'vitepress/theme'
import MyLayout from './components/MyLayout.vue';
export default {
extends: DefaultTheme,
Layout: MyLayout,
};
```
## 3.3 热力图
&emsp;&emsp;使用`vue3-calendar-heatmap`组件在首页绘制博客热力图。
### 3.3.1 安装
```sh
pnpm install vue3-calendar-heatmap
```
### 3.3.2 获取数据
&emsp;&emsp;使用`VitePress`提供的[createContentLoader](https://vitepress.dev/zh/guide/data-loading)来获取所有的`md`博客数据。
&emsp;&emsp;`.vitepress/data`新建`posts.data.js`
```js
import { createContentLoader } from 'vitepress'
export default createContentLoader('./**/*.md', {
includeSrc: true, // 包含原始 markdown 源
render: true, // 包含渲染的整页 HTML
transform(rawData) {
return rawData.filter(page => {
return page.url.includes('.html') && page.frontmatter.date
}).map((page) => {
// 对每个页面进行处理
return {
// 返回你需要的页面数据
title: page.frontmatter.title,
date: page.frontmatter.date,
url: page.url
}
})
}
})
```
&emsp;&emsp;通过判断路径是否包含`html`来确认是博客文档,并且筛选出元数据中包含日期的博客,然后返回相应内容。
### 3.3.3 博客统计组件
&emsp;&emsp;`.vitepress/theme/components`文件夹下新建博客信息组件`StatsChart.vue`
```vue
<template>
<div v-if="!isMobile()">
<calendar-heatmap
:values="heatmapData"
:end-date="new Date()"
no-data-text="暂无记录"
tooltip-unit=""
:round="2"
:locale="{
less: '少于',
more: '多于',
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
days: ['日', '一', '二', '三', '四', '五', '六'],
}"
class="blog-heatmap"
/>
<div class="heatmap-footer">
⏱️ 已坚持<span class="highlight">{{ getDaysDifference(new Date(), startBlogDate) }}</span>
| 📝 已创作<span class="highlight">{{ data.length }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { CalendarHeatmap } from 'vue3-calendar-heatmap';
import 'vue3-calendar-heatmap/dist/style.css';
import { data } from '../../data/posts.data.js'
import { isMobile, getDaysDifference, startBlogDate } from "../utils";
const dateCountMap = new Map<string, number>();
// 按日期分组统计
data.forEach(item => {
const date = item.date
if (dateCountMap.has(date)) {
dateCountMap.set(date, dateCountMap.get(date)! + 1);
} else {
dateCountMap.set(date, 1);
}
});
// 转换为目标格式
const heatmapData = Array.from(dateCountMap.entries()).map(([date, count]) => ({
date,
count
}))
</script>
<style scoped>
.blog-heatmap {
margin-top: 10px;
}
.blog-heatmap :deep(.vch__wrapper) {
font-family: 'CustomFont', sans-serif !important;
}
.blog-heatmap :deep(.vch__month__label) {
font-size: 8px !important;
}
.blog-heatmap :deep(.vch__day__label) {
font-size: 8px !important;
}
.heatmap-footer {
font-size: 15px;
color: #666;
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
}
.highlight {
font-weight: 700;
color: #6366f1;
margin: 0 2px;
font-size: 16px;
}
</style>
```
&emsp;&emsp;根据`posts.data.js`提供的博客数据进行日期分组,并统计已坚持天数和已创作博客数。
### 3.3.4 注册组件
&emsp;&emsp;`.vitepress/theme/index.ts`注册组件参考3.1节内容)。
### 3.3.5 使用组件
&emsp;&emsp;`docs/index.md`最后添加组件。
::: tip
如果在打包时遇到`Named export 'CalendarHeatmap' not found. The requested module 'vue3-calendar-heatmap' is a CommonJS module, which may not support all module.exports as named exports.`错误提示,说明`vue3-calendar-heatmap``CommonJS`模块,而你在代码中使用了`ES`模块的命名导入方式,需要在`config.mts`中添加以下配置:
```ts
export default defineConfig({
vite: {
ssr: {
noExternal: ['vue3-calendar-heatmap']
}
}
})
```
:::
## 3.4 Mermaid 图表
### 3.4.1 安装
```sh
pnpm i vitepress-plugin-mermaid mermaid -D
```
### 3.4.2 配置
&emsp;&emsp;`.vitepress/config.mts`中配置:
```ts
import { withMermaid } from 'vitepress-plugin-mermaid'
// https://vitepress.dev/reference/site-config
export default withMermaid({
title: "拾光记"
})
```
### 3.4.3 使用
&emsp;&emsp;在`Markdown`中使用:
```md
'''mermaid
graph TD
A[Vue 3] --> B[Compiler]
A --> C[Runtime]
A --> D[Reactivity]
B --> B1[Parser]
B --> B2[Transformer]
B --> B3[Codegen]
C --> C1[Virtual DOM]
C --> C2[Renderer]
C --> C3[Component]
D --> D1[Proxy]
D --> D2[Effect]
D --> D3[Dependency Graph]
B3 -->|生成| C1
C1 -->|Diff/Patch| C2
D3 -->|驱动更新| C1
'''
```
## 3.5 图片预览
### 3.5.1 安装
```sh
pnpm i vitepress-plugin-image-viewer viewerjs
```
### 3.5.2 配置
&emsp;&emsp;`.vitepress/theme/index.ts`中配置:
```ts
import imageViewer from "vitepress-plugin-image-viewer";
import "viewerjs/dist/viewer.min.css";
export default {
extends: DefaultTheme,
setup() {
const route = useRoute();
imageViewer(route);
}
};
```

509
docs/Others/WeCom.md Normal file
View File

@@ -0,0 +1,509 @@
---
title: 企业微信工作台开发
date: 2026-01-23
---
# 一、建立工作台
1. 进入企业微信后台管理系统,选择应用管理->应用管理。
2. 选择自建->创建应用。
3. 输入名称、图标等信息,分配可见范围,完成应用创建.
4. 点击应用,选择开发者接口->网页授权配置可信域名和企业可信IP。
5. 查看并保存AgentId和Secret。
::: tip
可信域名不能是IP形式必须是和企业微信备案主体一致的服务器。
:::
# 二、JS-SDK开发
## 2.1 接口鉴权
&emsp;&emsp;[官方文档](https://developer.work.weixin.qq.com/document/path/90514)
&emsp;&emsp;参考第三节企业微信鉴权接口开发
## 2.2 打开默认浏览器
&emsp;&emsp;使用系统浏览器打开指定 URL支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。
# 三、鉴权接口开发
&emsp;&emsp;本教程以node.js为例。
## 3.1 通用企业微信服务端API
```js
const axios = require('axios');
const TOKEN_URL = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
const JSAPI_TICKET_URL = 'https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket'
const MESSAGE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
// 获取access_token
async function getAccessTokenApi(corpId, corpSecret) {
const now = Date.now();
try {
const url = `${TOKEN_URL}?corpid=${corpId}&corpsecret=${corpSecret}`
const response = await axios.get(url);
if (response.data.errcode === 0) {
return {
accessToken: response.data.access_token,
expires: now + (response.data.expires_in - 300) * 1000 // 提前5分钟过期
};
} else {
throw new Error(`获取access_token失败: ${response.data.errmsg}`);
}
} catch (error) {
console.error('获取access_token错误:', error);
throw error;
}
}
// 获取jsapi_ticket
async function getJsapiTicketApi(accessToken) {
try {
const url = `${JSAPI_TICKET_URL}?access_token=${accessToken}`
const response = await axios.get(url);
if (response.data.errcode === 0) {
return response.data.ticket;
} else {
throw new Error(`获取jsapi_ticket失败: ${response.data.errmsg}`);
}
} catch (error) {
console.error('获取jsapi_ticket错误:', error);
throw error;
}
}
// 发送应用消息
async function sendMessageApi(agentid, accessToken, touser, content) {
try {
const url = `${MESSAGE_URL}?access_token=${accessToken}`
const response = await axios.post(url, {
touser: touser,
msgtype: 'text',
agentid: agentid,
text: {
content: content
}
});
if (response.data.errcode === 0) {
return true;
} else {
throw new Error(`发送应用消息失败: ${response.data.errmsg}`);
}
} catch (error) {
console.error('发送应用消息错误:', error);
throw error;
}
}
module.exports = {
getAccessTokenApi,
getJsapiTicketApi,
sendMessageApi
};
```
## 3.2 自建应用鉴权接口
```js
const {getAccessTokenApi, getJsapiTicketApi, sendMessageApi} = require("./wecom");
const config = {
corpId: '', // 企业id
corpSecret: '', // 应用密码
agentId: '', // 应用id
domain: '', // 跳转域名
password: '' // 登录默认密码
};
// 存储access_token和jsapi_ticket
let tokenCache = {
accessToken: null,
jsapiTicket: null,
expires: 0
};
// 获取access_token
async function getRepairAccessToken() {
const now = Date.now();
if (tokenCache.accessToken && tokenCache.expires > now) {
return tokenCache.accessToken;
}
const result = await getAccessTokenApi(config.corpId, config.corpSecret)
tokenCache.accessToken = result.accessToken;
tokenCache.expires = result.expires
}
// 获取jsapi_ticket
async function getRepairJsapiTicket() {
const now = Date.now();
if (tokenCache.jsapiTicket && tokenCache.expires > now) {
return tokenCache.jsapiTicket;
}
await getRepairAccessToken();
tokenCache.jsapiTicket = await getJsapiTicketApi(tokenCache.accessToken);
}
// 发送应用消息
async function sendRepairMessage(touser, content) {
await getRepairAccessToken();
await sendMessageApi(config.agentId, tokenCache.accessToken, touser, content);
}
function getRepairDomain() {
return config.domain
}
function getRepairPassword() {
return config.password
}
module.exports = {
getRepairAccessToken,
getRepairJsapiTicket,
sendRepairMessage,
getRepairDomain,
getRepairPassword
};
```
## 3.3 NodeJS API
```js
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const {generateNonceStr, generateSignature} = require("./utils");
const {sendRepairMessage, getRepairJsapiTicket, getRepairAccessToken, getRepairDomain, getRepairPassword} = require("./repair");
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
const corpId = 'ww48932728388c9e3a'
const USER_INFO_URL = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
// API接口生成JS-SDK配置
app.post('/auth/config/repair', async (req, res) => {
await configService(req, res, 'repair')
});
async function configService(req, res, appName) {
try {
const {url} = req.body;
if (!url) {
return res.status(400).json({errcode: -1, errmsg: '缺少url参数'});
}
let jsapiTicket = ''
if (appName === 'repair') {
jsapiTicket = await getRepairJsapiTicket();
}
const timestamp = Math.floor(Date.now() / 1000);
const noncestr = generateNonceStr();
const signature = generateSignature(jsapiTicket, noncestr, timestamp, url);
res.json({
errcode: 0,
errmsg: 'ok',
appId: corpId,
timestamp,
nonceStr: noncestr,
signature
});
} catch (error) {
console.error('生成JS-SDK配置错误:', error);
res.status(500).json({errcode: -1, errmsg: '服务器内部错误'});
}
}
// API接口OAuth回调处理
app.get('/auth/callback/repair', async (req, res) => {
await callback(req, res, 'repair')
});
async function callback(req, res, appName) {
try {
const {code, state} = req.query;
if (!code) {
return res.status(400).send('缺少code参数');
}
// 通过code获取用户信息
let accessToken = ''
if (appName === 'repair') {
accessToken = await getRepairAccessToken();
}
const url = `${USER_INFO_URL}?access_token=${accessToken}&code=${code}`
const userResponse = await axios.get(url);
if (userResponse.data.errcode !== 0) {
throw new Error(`获取用户信息失败: ${userResponse.data.errmsg}`);
}
const userInfo = userResponse.data;
// 重定向到前端页面
let redirectUrl = ''
if (appName === 'repair') {
redirectUrl = `${getRepairDomain()}?username=${userInfo.UserId}&password=${getRepairPassword()}&type=repair`;
}
res.redirect(redirectUrl);
} catch (error) {
console.error('OAuth回调处理错误:', error);
res.status(500).send(`
<html>
<body>
<h1>登录失败</h1>
<p>${error.message}</p>
</body>
</html>
`);
}
}
// API接口发现应用消息
app.post('/message/repair', async (req, res) => {
const {touser, content} = req.body;
await sendRepairMessage(touser, content)
res.json({
errcode: 0,
errmsg: 'ok'
});
});
// 启动服务
app.listen(PORT, () => {
console.log(`企业微信登录服务运行在端口 ${PORT}`);
});
module.exports = app;
```
## 3.4 工具类
```js
const crypto = require('crypto');
// 工具函数:生成随机字符串
function generateNonceStr(length = 16) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// 工具函数:生成签名
function generateSignature(jsapiTicket, noncestr, timestamp, url) {
const string = `jsapi_ticket=${jsapiTicket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`;
return crypto.createHash('sha1').update(string).digest('hex');
}
module.exports = {
generateNonceStr,
generateSignature
};
```
# 四、应用主页
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业微信登录跳转</title>
<!-- 引入新版企业微信JS-SDK -->
<script src="https://wwcdn.weixin.qq.com/node/open/js/wecom-jssdk-2.3.3.js"></script>
</head>
<body>
<div id="loading">正在跳转,请稍候...</div>
<script>
// 配置参数
const serverUrl = ''
const config = {
corpId: '', // 企业ID
agentId: '', // 应用ID
redirectUrl: encodeURIComponent(`${serverUrl}auth/callback/repair`), // OAuth回调地址
authUrl: `${serverUrl}auth/config/repair` // 后端授权入口
};
// 主函数
async function init() {
try {
// 获取当前页面URL用于签名
const currentUrl = window.location.href.split('#')[0];
// 从后端获取签名信息
const signatureData = await getSignatureData(currentUrl);
const getConfigSignature = function(url) {
return {
timestamp: signatureData.timestamp,
nonceStr: signatureData.nonceStr,
signature: signatureData.signature
};
};
// 使用ww.register 进行身份注册
await ww.register({
corpId: config.corpId,
agentId: config.agentId,
jsApiList: ['openDefaultBrowser', 'closeWindow'],
getConfigSignature: getConfigSignature
});
// 生成授权URL
const authUrl = await generateAuthUrl();
const isMobile = /(phone|pad|pod|iphone|ipod|ios|ipad|android|mobile|blackberry|iemobile|mqqbrowser|fennec|wosbrowser|browserng|webos|symbian|windows phone)/i.test(navigator.userAgent);
if (isMobile) {
// 在移动端企业微信中直接打开链接
window.location.href = authUrl;
} else {
// 使用新版API调用系统浏览器
ww.openDefaultBrowser({
url: authUrl,
success: (res) => {
// console.log('成功调用系统浏览器:', res);
ww.closeWindow({})
},
fail: (err) => {
// console.error('打开浏览器失败:', err);
document.getElementById('loading').innerHTML = `打开失败: ${err.errMsg || '未知错误'},请点击右上角菜单选择"在浏览器中打开"`;
}
});
}
} catch (error) {
console.error('初始化失败:', error);
document.getElementById('loading').innerHTML = '跳转失败,请手动在浏览器中打开: ' + error.errMsg;
// window.alert(error.errMsg)
}
}
// 获取签名数据
async function getSignatureData(url) {
const response = await fetch(`${config.authUrl}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: url
})
});
if (!response.ok) {
throw new Error('获取签名配置失败');
}
const result = await response.json();
// 确保返回的数据包含必要的字段
if (!result.timestamp || !result.nonceStr || !result.signature) {
throw new Error('签名数据不完整');
}
return result;
}
// 生成授权URL
async function generateAuthUrl() {
// 使用URLSearchParams构建参数
const authParams = new URLSearchParams({
appid: config.corpId,
redirect_uri: config.redirectUrl,
response_type: 'code',
scope: 'snsapi_base', // 静默授权
state: 'wecom_redirect',
agentid: config.agentId
});
return `https://open.weixin.qq.com/connect/oauth2/authorize?${authParams.toString()}#wechat_redirect`;
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
```
&emsp;&emsp;将该应用主页的URL放在工作台的应用主页中。
::: tip
需要在前端工程中配置路由参数访问即通过ip:port/#/login?username=''&password=''的形式访问。
:::
::: tip
如果需要在发送应用消息时可以点击消息访问应用绑定的URL链接也是该应用主页的链接。
:::
# 五、使用
&emsp;&emsp;本教程以SpringBoot为例。
## 5.1 配置RestClient
```java
@Configuration
public class RestClientConfig {
final String weiComBaseUrl = "";
@Bean
public RestClient weiComClient() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(5));
factory.setReadTimeout(Duration.ofSeconds(10));
return RestClient.builder()
.requestFactory(factory)
.baseUrl(weiComBaseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
```
## 5.2 调用发送消息接口
```java
@Service
@Slf4j
public class NoticeService {
@Resource
private RestClient weiComClient;
@Async
public void sendRepairMessage(String touser, String content) {
String link = "";
WeComNoticeRequest request = new WeComNoticeRequest();
request.setTouser(touser);
request.setContent(content + "\n点击打开工作台应用" + "<a href=\"" + link + "\">打开应用</a>");
WeComResponse response = weiComClient.post()
.uri("/message/repair")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(WeComResponse.class);
if (response == null || response.getErrcode() != 0) {
throw new CustomException("企业微信发送消息失败 请联系管理员");
}
}
}
```
# 附录
企业微信官方文档:[接口](https://developer.work.weixin.qq.com/document/path/90664)

33
docs/Others/XDRP.md Normal file
View File

@@ -0,0 +1,33 @@
---
title: XDRP简介和安装
date: 2026-02-25
---
# 一、简介
&emsp;&emsp;XDRP 是一个基于 RDPRemote Desktop Protocol 远程桌面协议)的开源服务器软件,允许用户通过远程桌面客户端连接到 Linux 服务器,提供图形化远程访问功能。
&emsp;&emsp;特点:
1. 支持 Windows 远程桌面客户端
2. 支持多种桌面环境GNOME、KDE、XFCE 等
3. 使用标准的 3389 端口
4. 轻量级,资源占用较少
5. 不需要连接显示器
# 二、安装
```bash
sudo apt update
sudo apt install xrdp -y
sudo systemctl enable xrdp
```
# 三、连接
&emsp;&emsp;打开Windows自带的远程桌面连接工具输入ip、用户名和密码登录。
::: tip
如果遇到黑屏的情况,打开/etc/xrdp/startwm.sh脚本`if test -r /etc/default/locale; then fi`这段结束后,插入以下内容:
```bash
unset DBUS_SESSION_BUS_ADDRESS
unset XDG_RUNTIME_DIR
. $HOME/.profile
```
然后sudo systemctl restart xrdp重启服务。
:::

View File

@@ -7,4 +7,4 @@ title: 其他
import { routers } from '../.vitepress/theme/router'
</script>
<MenuList :routers=routers[4] />
<MenuList :routers=routers[3] />

View File

@@ -365,7 +365,7 @@ public abstract class AbstractException extends RuntimeException {
```
:::
### 3.4 starters模块
## 3.4 starters模块
&emsp;&emsp;该模块为整个项目的核心模块在实际项目中通过引入相应的starter模块并配合autoconfigure模块中的功能即可实现快速自动装配。
&emsp;&emsp;该模块的pom.xml声明的依赖会注入到实际的项目中。
::: danger
@@ -373,7 +373,7 @@ public abstract class AbstractException extends RuntimeException {
之前的SringBoot2是在**main->resources->META-INF**目录下新建spring.factories文件。
:::
#### 3.4.1 stater-jdbc
### 3.4.1 stater-jdbc
&emsp;&emsp;该模块主要提供数据库相关的配置功能。
&emsp;&emsp;目前实现的功能有:
1. 配置MybatisPlus拦截器添加乐观锁和分页插件。
@@ -450,7 +450,7 @@ public class MybatisMetaObjectHandler implements MetaObjectHandler {
&emsp;&emsp;`@ConditionalOnClass(SqlSessionFactory.class)`表示只有存在SqlSessionFactory类时这个配置类才会生效。即如果没有 MyBatis 相关依赖,这个配置类会被 Spring 完全忽略。
&emsp;&emsp;`@Import`用于导入其他配置类或组件到当前配置类中。
#### 3.4.2 starter-web
### 3.4.2 starter-web
&emsp;&emsp;该模块主要提供web相关的配置功能。
&emsp;&emsp;目前实现的功能有:
1. 注册审计拦截器
@@ -748,6 +748,162 @@ HandlerInterceptor 拦截器执行顺序:
4. afterCompletion():视图渲染后 按注册顺序逆序执行。
:::
### 3.4.3 starter-logging
&emsp;&emsp;`src->main->resources`中添加`logback-spring.xml`文件:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 自定义属性 -->
<springProperty scope="context" name="fluentdHost" source="logging.fluentd.host" defaultValue="localhost"/>
<springProperty scope="context" name="fluentdPort" source="logging.fluentd.port" defaultValue="24224"/>
<springProperty scope="context" name="topic" source="spring.application.name" defaultValue="default"/>
<!-- log路径 -->
<property name="log.path" value="logs/${topic}"/>
<!--控制台日志格式:彩色日志-->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
<!--文件日志格式-->
<property name="FILE_LOG_PATTERN"
value="%date{yyyy-MM-dd HH:mm:ss} |%-5level |%thread |%file:%line |%logger |%msg%n"/>
<!--编码-->
<property name="ENCODING"
value="UTF-8"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--日志级别-->
<level>DEBUG</level>
</filter>
<encoder>
<!--日志格式-->
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!--日志字符集-->
<charset>${ENCODING}</charset>
</encoder>
</appender>
<!--输出到文件-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志过滤器此日志文件只记录INFO级别的-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志文件的最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志过滤器此日志文件只记录WARN级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志文件的最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志过滤器此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志文件的最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<!--输出到FLUENCY-->
<appender name="FLUENCY" class="ch.qos.logback.more.appenders.FluencyLogbackAppender">
<remoteHost>${fluentdHost}</remoteHost>
<port>${fluentdPort}</port>
<additionalField>
<key>topic</key>
<value>${topic}</value>
</additionalField>
<ignoredField>caller</ignoredField>
<useEventTime>true</useEventTime>
</appender>
<!-- 开发环境 -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 生产环境 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- 容器环境 -->
<springProfile name="docker">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FLUENCY"/>
</root>
</springProfile>
</configuration>
```
&emsp;&emsp;该文件会根据当前环境自动添加日志输出节点。
# 四、项目发布
## 4.1 发布到Git中

View File

@@ -1,5 +1,5 @@
---
title: Vue3-Common
title: Vue3 Common
date: 2025-12-15 23:25
---
@@ -32,7 +32,7 @@
:::
# 二、vite.config.ts配置
## 2.1 生成Typescript类型文件
## 2.1 生成ts类型文件
&emsp;&emsp;安装vite-plugin-dts插件
```cmd
@@ -377,6 +377,167 @@ const app = createApp(App)
app.use(router)
```
8. 在layout中使用
```vue
<template>
<aside class="layout-sidebar">
<menu-list
:route="useRoute()"
:routers="menuRoutes"
:collapse="isCollapse"
class="layout-menu"/>
</aside>
</template>
<script lang="ts" setup>
import { MenuList } from 'vue3-common'
import menuRoutes from '@/router/menu'
import { useRoute } from 'vue-router'
</script>
```
&emsp;&emsp;如果需要对路由菜单进行权限处理,可以使用计算属性过滤一遍:
```ts
const routers = computed(() => {
const menuPathList = menuRoutes.map((item) => item.path)
const removePathList = menuPathList.filter((item) => !authStore.currentUser.menu.includes(item))
removePathList.forEach((item) => {
router.removeRoute(capitalizeStr(item.replace('/', '')))
})
return menuRoutes.filter((route) => !removePathList.includes(route.path))
})
```
::: tip
实现原理:
```ts
import type { RouteComponent, RouteRecordRaw, RouteMeta } from 'vue-router'
/**
* 任意对象类型
*/
interface ICommonObj {
[key: string]: any;
}
/**
* 路由meta配置
*/
interface IRouteMetaConfig {
[key: string]: RouteMeta;
}
/**
* 路由路径转为名称 /admin/dashboard -> AdminDashboard
* @param path
*/
export const convertPath2Name = (path: string): string => {
// 移除路径开头和结尾的斜杠(如果存在)
const newPath = path.replace(/^\/|\/$/g, '')
// 将路径按斜杠分割成多个部分
const parts = newPath.split('/')
// 处理每个部分:将连字符后的字母大写,并移除连字符;处理动态参数(如 :id
return parts.map((part) =>
part.split('-').map((subPart) => {
// 处理动态参数(如 :id
if (subPart.startsWith(':')) {
return subPart.slice(1).charAt(0).toUpperCase() + subPart.slice(2)
}
// 普通部分
return subPart.charAt(0).toUpperCase() + subPart.slice(1)
}).join('')).join('')
}
/**
* 根据meta的order排序
* @param routes 路由列表
*/
export const sortRoutesByOrder = (routes: RouteRecordRaw[]): RouteRecordRaw[] => {
// 排序当前路由的子路由
routes.forEach((route) => {
if (route.children && route.children.length > 0) {
// 如果有子路由,递归排序子路由
route.children = sortRoutesByOrder(route.children)
}
})
// 对当前路由按 meta.order 排序
return routes.sort((a, b) => (a.meta?.order as number || 0) - (b.meta?.order as number || 0))
}
/**
* 获取路由
* @param modules 通过import.meta.glob导入的模块
* @param layout 布局组件
* @param metaConfig 路由meta配置
*/
export const getRoutersByModules = (modules: ICommonObj,
layout: RouteComponent, metaConfig?: IRouteMetaConfig): RouteRecordRaw[] => {
// const modules = import.meta.glob('@/views/admin/**/*.vue')
const routes: RouteRecordRaw[] = []
Object.keys(modules).forEach((filePath) => {
// 获取文件的模块路径和文件名
const parentPath = filePath.substring(0, filePath.lastIndexOf('/')).replace('/src/views', '')
const path = filePath.replace('.vue', '')
.replace('/src/views', '')
.replace(/\[([^\]]+)]/g, '/:$1')
// 为每个文件生成对应的路由
const route: RouteRecordRaw = {
path,
name: convertPath2Name(path),
component: modules[filePath]
}
// 如果存在meta配置
if (metaConfig) {
route.meta = metaConfig[path]
// 设置重定向属性
if (route.meta?.redirect) {
route.redirect = route.meta?.redirect as any
}
}
// 如果该路径已经存在(嵌套路由),则将其作为子路由
let parentRoute = routes.find((item) => item.path === parentPath)
if (!parentRoute) {
parentRoute = {
path: parentPath,
name: convertPath2Name(parentPath),
component: layout,
children: []
}
// 如果存在meta配置
if (metaConfig) {
parentRoute.meta = metaConfig[parentPath]
// 设置重定向属性
if (parentRoute.meta?.redirect) {
parentRoute.redirect = parentRoute.meta?.redirect
}
}
routes.push(parentRoute)
}
// 如果是子路由,加入到父路由的 `children` 数组
parentRoute.children?.push(route)
})
return routes
}
```
&emsp;&emsp;核心原理就是利用`import.meta.glob`创建路径->组件的映射,然后结合路由配置表,转为路由对象并添加到`vue router`中。
:::
## 5.2 图标组件
1. 安装vite-plugin-svg-icons依赖
```cmd

View File

@@ -0,0 +1,62 @@
---
title: 埃学习
date: 2026-05-27
---
# 一、项目背景
&emsp;&emsp;为解决企业内部线下培训成本高、进度难追踪的问题,开发了一套企业在线学习平台,用于员工课程学习、在线考试、培训计划管理与数据统计分析,已全面接入企业微信工作台。
# 二、系统架构
![架构图](../../public/images/aixuexi/arch.png)
# 三、核心模块
## 3.1 员工端
1. 课程学习(视频播放、进度记录、课程评论)
2. 在线考试(随机试卷、自动阅卷)
3. 学习记录和统计
## 3.2 管理员端
1. 课程管理(视频上传、章节管理)
2. 培训计划管理(按部门/岗位下发)
3. 考试与题库管理
4. 人员与组织管理
5. 员工学习数据统计
# 四、技术栈
| 层级 | 技术 |
|---|---|
| **前端** | Vue 3、Element Plus、Vue Router、Pinia、DPlayer (视频播放)、@vue-office (文档预览)、ECharts (数据可视化)|
| **后端** | Spring Boot 3、Spring MVC、Sa-Token (鉴权)、MyBatis-Plus |
| **中间件** | Redis、企业微信 SDK |
| **视频处理** | Java JAVE2 (FFmpeg 封装,视频压缩转码) |
| **存储** | MySQL 8.0 (业务数据)、AList (视频/文件资源存储) |
| **运维** | Docker、Nginx、Fluent Bit (日志采集)、OpenObserver (日志可视化) |
# 五、核心设计
- 基于 **Spring Boot 3 + MyBatisPlus** 构建稳定的后端架构,通过 **SaToken** 实现接口级鉴权,保障系统安全与权限可控。
- 集成 **企业微信 SDK**,实现企业微信工作台免登录认证;通过**企业微信消息通知机制**,自动推送学习任务与提醒。
- 设计 **课程资源、学习计划与在线考试** 等核心业务模型,支持批量下发培训任务;实现随机组卷、自动阅卷与成绩统计,并自动提醒员工完成学习任务。
- 基于 **JAVE2FFmpeg** 实现视频自动压缩与转码,适配多终端播放;采用 AList 作为统一文件存储平台,集中管理音视频及课件资源。
- 基于 **Vue 3 + Element Plus + Pinia** 构建管理员端与员工端页面,集成 **vueoffice** 实现多格式文档在线预览,集合**ECharts**实现多维度数据分析。
- 使用 **Docker + Nginx** 部署,结合 **Fluent Bit + OpenObserver** 构建统一日志监控体系,提升系统可观测性与运维效率。
# 六、难点/亮点
## 5.1 公网视频播放卡顿
- **问题**:服务器公网带宽小,高清视频并发播放严重卡顿。
- **方案**上传视频后使用JAVE2调用FFmpeg将视频统一压缩为720p的清晰度视频。
- **效果**视频体积减少60%+,公网环境播放流畅。
## 5.2 学习进度防作弊
- **问题**:员工学习视频课程时,可通过拖动进度条快速完成课程,无法真实反映学习行为,存在“刷课”风险。
- **方案**:根据管理员设定的课程学习时长,在员工学习时,启动定时器;每分钟会保存学习时长至数据库中,下次学习时,直接跳转至上次学习进度;课程是否完成已定时器结束为准。
- **效果**:有效防止通过拖拽进度条刷课,保证学习时长真实可信;同时支持断点续学,避免重复学习,满足企业培训考核与合规审计要求。
## 5.3 学习计划通知
- **问题**:培训计划、考试通知无法及时触达员工,导致培训参与率低、管理成本高。
- **方案**:在发布培训计划时,调用企业微信消息推送接口,自动向指定员工发送应用消息通知;结合定时任务,对未完成培训的人员进行周期性提醒。
- **效果**:培训通知实时触达,未读提醒显著提高培训完成率;管理员无需手动维护人员信息,降低了系统运维成本。
# 七、成果
1. 平台全面替代线下培训,年度培训计划完成率由 60% 提升至 95% 以上,满足了企业合规培训要求。
2. 过视频压缩方案解决了公网带宽瓶颈,结合企业微信自动化通知,大幅降低人工催办成本,整体培训运营成本下降约 40%。
3. 实现了学习进度防作弊与学习记录全跟踪,为公司提供精准的数据统计报表,支撑人才盘点与绩效评估。

View File

@@ -0,0 +1,88 @@
---
title: 个人博客系统
date: 2026-06-08
---
# 一、项目背景
&emsp;&emsp;为满足个人技术积累与内容分享需求,独立开发了一套多端博客系统,支持博客管理、发布与多端阅读。
&emsp;&emsp;系统包含博客网站、博客管理后台与移动端 App覆盖 Web 端与移动端用户场景,实现了从内容创作、存储、检索到多端展示的完整闭环。
# 二、系统架构
# 三、核心模块
## 3.1 博客网站
1. 首页博客列表展示(分页、置顶)
2. 博客分类与标签筛选
3. 博客归档(按时间维度)
4. 博客全文搜索Elasticsearch
5. Markdown 文章渲染与代码高亮
6. 博客阅读时长和阅读人数统计
7. 评论发布与互动(匿名)
<VPSwiper
:slides="[
'/blog-press/images/blog/home.png',
'/blog-press/images/blog/category.png',
'/blog-press/images/blog/archive.png',
'/blog-press/images/blog/search.png',
'/blog-press/images/blog/content.png',
'/blog-press/images/blog/commit.png'
]"
button-text="点击全屏"
:navigation="false"
effect="coverflow"
/>
## 3.2 博客管理后台
1. 博客新增、编辑、删除(权限控制)
2. Markdown 编辑器md-editor-v3
3. 博客图片上传RustFS
4. 分类 / 标签管理
5. 博客状态管理(草稿 / 发布)
6. 博客访问数据查看
7. 统计数据仪表盘ECharts
<VPSwiper
:slides="[
'/blog-press/images/blog/login.png',
'/blog-press/images/blog/stats.png',
'/blog-press/images/blog/article.png',
'/blog-press/images/blog/edit.png'
]"
button-text="点击全屏"
:navigation="false"
effect="coverflow"
/>
## 3.3 移动端APP
1. 博客列表与详情浏览
2. Markdown 内容原生渲染markdown_widget
3. 分类、归档、搜索功能
4. 移动端自适应布局
# 四、技术栈
| 层级 | 技术 |
|---|---|
| **前端** | Vue 3、Element Plus、Vue Router、Pinia、md-editor-v3、Axios |
| **后端** | FastAPI、Pydantic、SQLAlchemy、Uvicorn |
| **移动端** | Flutter、markdown_widget、provider、dio |
| **中间件** | Elasticsearch全文检索、分词索引 |
| **存储** | MySQL 8.0业务数据、RustFS图片存储 |
| **运维** | Docker、Nginx、Fluent Bit (日志采集)、OpenObserver (日志可视化) |
# 五、核心设计
- 基于 FastAPI 构建高性能后端 API采用分层结构设计保证业务逻辑清晰、接口响应高效。
- 博客内容统一采用 Markdown 格式Web 端与移动端分别基于 md-editor-v3与 markdown_widget实现一致的内容渲染效果。
- 使用 Elasticsearch 构建博客全文检索引擎,支持标题、内容、标签多维搜索,显著提升内容查找效率。
- 图片资源统一上传至 RustFS 对象存储,通过 URL 访问,降低应用服务器存储压力,提升静态资源加载速度。
- 管理后台与博客前端共享同一后端服务,通过接口级权限控制,实现“管理操作需鉴权、公开访问无需登录”的安全设计。
- Web 端采用 Vue 3 + Pinia 实现状态管理与组件化开发;移动端基于 Flutter 实现跨平台一致体验。
# 六、难点 / 亮点
## 6.1 博客搜索性能优化
- **问题**随着博客数量增长MySQL LIKE 查询性能下降明显。
- **方案**:引入 Elasticsearch构建博客索引支持中文分词与多字段检索。
## 6.2 图片存储与访问优化
- **问题**:本地存储图片不利于扩展,且增加服务器带宽压力。
- **方案**:集成 RustFS 对象存储,所有博客图片统一上传并返回访问 URL。

View File

@@ -0,0 +1,65 @@
---
title: 家庭管理系统
date: 2026-06-04
---
# 一、项目背景
&emsp;&emsp;针对家庭信息分散、难以统一管理的问题,设计并实现了一套移动端家庭管理平台,将账单、旅行、健康、纪念日等数据进行集中管理,并通过私有化部署与数据加密保障安全,实现家庭生活的数字化沉淀。
# 二、系统架构
# 三、核心模块
- **账单记录**
![账单记录](../../public/images/sweethut/bill.jpg)
- **美食记录**
- **旅行记录**
- **物品管理**
- **运动健康**
- **纪念日管理**
- **家庭资产**
- **密码管家**
- **奖状证书**
- **时光手账**
# 四、技术栈
| 层级 | 技术 |
|---|---|
| **前端** | Vue 3、Vant、Vue Router、Pinia、Axios |
| **后端** | Spring Boot 3、Spring MVC、Satoken、MyBatisPlus |
| **中间件** | Express +Sharp图片缩略图、Quartz定时任务 |
| **存储** | MySQL 8.0业务数据、AList图片/视频对象存储) |
| **视频处理** | Java JAVE2 (FFmpeg 封装,视频压缩转码) |
| **安全** | AES 数据加密 |
| **移动端** | FlutterWebView 封装为 Android APK |
| **运维** | Docker、Nginx、Fluent Bit (日志采集)、OpenObserver (日志可视化) |
# 五、核心设计
- 基于 **Spring Boot 3 + MyBatisPlus** 构建稳定的后端架构,通过 **SaToken** 实现接口级鉴权,保障系统安全与权限可控。
- 所有敏感信息(密码、资产等)均使用 **AES 对称加密** 存储,防止数据库泄露导致隐私暴露。
- 使用 **Node.js + Sharp** 搭建图片服务,移动端优先加载缩略图,点击后按需加载原图,显著优化首屏性能。
- 视频资源上传后通过 **JAVE2 + FFmpeg** 压缩并转码,在保证清晰度的前提下减小体积。
- 基于 **Quartz** 实现定时任务,用于纪念日提醒等场景。
- 前端基于 **Vue 3 + Element Plus + Vant + Pinia** 开发移动端页面,并使用 **Flutter WebView** 将 Web 应用封装为 Android APK实现一套代码多端使用。
- 使用 **Docker + Nginx** 部署,结合 **Fluent Bit + OpenObserver** 构建统一日志监控体系,提升系统可观测性与运维效率。
# 六、难点 / 亮点
## 6.1 图片资源加载性能优化
- **问题**:手机拍摄的照片体积较大,移动端直接加载原图导致页面卡顿、流量消耗大。
- **方案**:使用 **Node.js + Sharp** 构建图片实时处理服务通过路径参数实时获取压缩图片默认加载缩略图点击后加载原图并配合lazy懒加载优化首屏加载时间。
## 6.2 视频资源压缩与存储
- **问题**:手机视频多为高清录制,体积大、加载慢,直接存储与播放影响用户体验。
- **方案**:使用 **JAVE2 + FFmpeg** 在上传时将视频统一压缩并转码,减少视频空间。
## 6.3 敏感数据加密存储
- **问题**:密码、资产等数据高度敏感,若数据库被攻破将造成严重隐私泄露。
- **方案**:采用 AES 对敏感字段进行加密存储,密钥与应用配置隔离管理,服务端统一加解密。

View File

@@ -1,10 +1,10 @@
---
layout: doc
title: Flutter
title: 项目实战
---
<script setup>
import { routers } from '../.vitepress/theme/router'
</script>
<MenuList :routers=routers[3] />
<MenuList :routers=routers[2] :isNested=true />

View File

@@ -1,11 +0,0 @@
---
layout: doc
title: Web后端
description: Web后端开发技术文档包含数据库、框架、中间件等
---
<script setup>
import { routers } from '../.vitepress/theme/router'
</script>
<MenuList :routers=routers[1] :isNested=true />

242
docs/Web/AI/Langchain.md Normal file
View File

@@ -0,0 +1,242 @@
---
title: 基于Langchain的Agent开发
date: 2026-05-28
---
# 一、简介
&emsp;&emsp;LangChain是一整个智能体开发平台包含一系列开源的智能体Agent开发框架而且兼容Python和TypeScript两种语言。
&emsp;&emsp;Agent通常翻译为智能体或代理是指一种能够感知环境、进行推理、自主决策并采取行动以实现特定目标的智能系统。
# 二、模型
## 2.1 模型定义
&emsp;&emsp;通用自定义模型`init_chat_model`
```python
# 初始化模型
model = init_chat_model(
model="qwen-max", # 模型名称这里可以自定义我们用的是阿里的qwen-max
model_provider="openai",
base_url=base_url,
api_key=api_key,
temperature=1.5,
)
```
&emsp;&emsp;如果是Langchain不支持的模型需要指定模型提供者虽然我们用的是阿里但是阿里兼容openai所以这里用openai就是默认采用openai的API规范
::: tip
init_chat_model还可以调整模型参数
- temperature: 控制生成文本的随机性,值越小越确定,值越大越随机
- max_tokens: 控制生成文本的最大长度
- top_p: 控制生成文本的多样性,值越小越多样,值越大越确定
- timeout: 控制生成文本的超时时间
- max_retries: 控制生成文本的最大重试次数
:::
## 2.2 访问模型
### 2.2.1 阻塞调用
```python
# 调用invoke方法
response = model.invoke("中国的首都是哪里?")
# 查看响应结果
print(response)
```
### 2.2.2 流式调用
```python
# 通过.stream方法实现流式访问
stream = model.stream("中国的首都是哪里?")
# 遍历stream结果实时打印AI的回复
for chunk in stream:
print(chunk.content, end="", flush=True)
```
# 三、智能体
## 3.1 创建智能体
```python
# 1.使用init_chat_model初始化模型
model = init_chat_model(
model="qwen-max",
model_provider="openai",
base_url=base_url,
api_key=api_key,
temperature=1.5,
)
# 2.使用初始化好的model创建智能体
agent = create_agent(model=model)
```
## 3.2 消息类型
- SystemMessagerole是system代表系统消息用于设定模型角色和交互背景
- HumanMessagerole是user代表用户输入的消息
- AIMessagerole是assistant代表LLM生成的响应包含文本、工具调用、元数据
- ToolMessagerole是tool代表工具调用时产生的结果
## 3.3 调用模型
```python
# 调用Agent发送消息
response = agent.invoke({
"messages": [
HumanMessage(content="你好,我是虎哥"),
AIMessage(content="你好,虎哥,很高兴认识你。"),
HumanMessage(content="我的名字是什么?")
]
})
for message in response['messages']:
message.pretty_print()
```
# 四、提示词
&emsp;&emsp;发送给大模型的所有消息都可以称为提示词Prompt它直接影响模型的输出结果。
&emsp;&emsp;其中SystemMessage尤为重要我们把SystemMessage称为系统提示词System Prompt它可以给模型设定角色和本次聊天的背景对模型生成的内容有很大的影响。
&emsp;&emsp;从内容来说,提示词通常包含以下几个部分,通常按此顺序排列:
- 身份Identity描述AI的职责、沟通风格和总体目标。
- 说明Instructions请指导模型如何生成所需的响应。它应该遵循哪些规则模型应该做什么以及模型绝对不能做什么
- 示例Examples提供可能的输入示例以及模型期望的输出。
- 背景信息Context向模型提供生成响应所需的任何额外信息例如RAG的额外知识库数据或您认为特别相关的任何其他数据。
&emsp;&emsp;例如写一个智能生成菜谱的提示词:
```python
system_prompt = """
你是一名私人厨师。收到用户提供的食材照片或清单后,请按以下流程操作:
1.识别和评估食材:若用户提供照片,首先辨识所有可见食材。基于食材的外观状态,评估其新鲜度与可用量,整理出一份“当前可用食材清单”。
2.智能食谱检索:优先调用 web_search 工具,以“可用食材清单”为核心关键词,查找可行菜谱。
3.多维度评估与排序:从营养价值和制作难度两个维度对检索到的候选食谱进行量化打分,并根据得分排序,制作简单且营养丰富的排名靠前。
4.结构化方案输出:把排序后的食谱整理为一份结构清晰的建议报告,要包含食谱信息、得分、推荐理由、食谱的参考图片,帮助用户快速做出决策。
请严格按照流程,优先调用 web_search 工具搜索食谱,搜索不到的情况下才能自己发挥。
"""
```
&emsp;&emsp;提示词作为agent的参数传递
```python
agent = create_agent(model=model, system_prompt=system_prompt)
```
# 五、工具
&emsp;&emsp;一个完整的Agent至少要包含两个关键的部分
- 模型是Agent的大脑负责推理、分析规划任务步骤
- 工具是Agent的手脚负责执行任务与外界交互
&emsp;&emsp;例如使用专门用于给Agent提供Web搜索的工具Tavily
```python
# 初始化工具,并设置参数,具体参数设置参考官网
tool = TavilySearch(
tavily_api_key=key,
max_results=5,
topic="general"
)
# 创建智能体使用预定义工具tavily
agent = create_agent(
model=model,
tools=[tool],
system_prompt=system_prompt
)
```
::: tip
tavily_api_key可以通过访问官网免费获取。
:::
# 六、记忆
&emsp;&emsp;LangChain也提供了很多持久化存储的checkpointer例如
- SqlLiteSaver 基于sqlite存储
- PostgresSaver 基于Postgres存储
- CosmosDBSaver 使用Azure Cosmos DB的实现
## 6.1 SqlLiteSaver
### 6.1.1 初始化
```python
# 初始化checkpointer
checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db", check_same_thread=False))
# 自动建表
checkpointer.setup()
```
### 6.1.2 配置
```python
# 创建agent
agent = create_agent(
model=model,
checkpointer=checkpointer,
)
agent.stream({"messages": [message]},
{"configurable": {"thread_id": thread_id}},
stream_mode="messages"
):
```
&emsp;&emsp;调用模型的时候需要传递`thread_id`
::: tip
通常还需要构建一个`用户-问题标题`的数据库,方便查询用户的所有历史回答。
问题标题可以再调一次模型总结。
:::
# 七、实战
## 7.1 后端
```python
async def query_agent(request: ChatRequest):
try:
message = HumanMessage(content=request.message)
# 流式调用Agent
for chunk, metadata in agent.stream(
{"messages": [message]},
{"configurable": {"thread_id": request.thread_id}},
stream_mode="messages"
):
if isinstance(chunk, AIMessageChunk) and chunk.content:
yield chunk.content
# 总结对话标题并保存
if not exists_session(request.thread_id, request.username):
title = generate_title(get_messages(request.thread_id))
save_session(request.thread_id, request.username, title)
except Exception as e:
print(f"\n[错误]: {str(e)}")
yield "信息检索失败,请重新输入问题提问"
@router.post("/chat/stream")
async def chat_endpoint(request: ChatRequest):
"""流式对话"""
return StreamingResponse(
query_agent(request),
media_type="text/event-stream"
)
```
## 7.2 前端
```typescript
const sendMessage = async () => {
try {
const response = await fetch(
"/chief-agent-api/chat/stream",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message)
}
);
const reader = response.body!.getReader();
const decoder = new TextDecoder("utf-8");
let aiText = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
aiText += decoder.decode(value, { stream: true });
}
} finally {
loading = false;
}
};
```

211
docs/Web/AI/RAG.md Normal file
View File

@@ -0,0 +1,211 @@
---
title: RAG开发
date: 2026-06-24
---
# 一、工作流程
```text
原始文档 / 数据
【文档收集与清洗】
【文档切分Chunking
【向量化Embedding
【向量数据库存储】
用户自然语言问题
【检索系统】召回相关知识片段
【上下文组装】构建增强 Prompt
【大模型】基于知识生成回答
【引用 / 校验】
最终返回给用户(答案 + 来源)
```
# 二、实战
## 2.1 资料处理
### 2.1.1 pdf处理
&emsp;&emsp;[PaddleOCR](https://aistudio.baidu.com/paddleocr)是百度飞桨PaddlePaddle团队开源的产业级 OCR光学字符识别与文档智能开发套件。
```python
JOB_URL = "https://paddleocr.aistudio-app.com/api/v2/ocr/jobs"
TOKEN = ""
MODEL = "PP-OCRv5"
FILE_URL = ""
HEADERS = {
"Authorization": f"bearer {TOKEN}",
"Content-Type": "application/json",
}
# ========== 提交 OCR 任务 ==========
payload = {
"fileUrl": FILE_URL,
"model": MODEL,
"optionalPayload": {
"useDocOrientationClassify": False,
"useDocUnwarping": False,
"useChartRecognition": False,
},
}
print("🚀 Submitting OCR job...")
resp = requests.post(JOB_URL, json=payload, headers=HEADERS)
resp.raise_for_status()
job_id = resp.json()["data"]["jobId"]
print(f"✅ Job submitted, jobId={job_id}")
# ========== 轮询任务状态 ==========
while True:
r = requests.get(f"{JOB_URL}/{job_id}", headers=HEADERS)
r.raise_for_status()
data = r.json()["data"]
state = data["state"]
if state == "pending":
print("⏳ Job status: pending")
elif state == "running":
prog = data.get("extractProgress", {})
total = prog.get("totalPages")
done = prog.get("extractedPages")
if total and done:
print(f"📄 Processing: {done}/{total} pages")
else:
print("📄 Processing...")
elif state == "done":
prog = data["extractProgress"]
print(
f"✅ Job finished | Pages: {prog['extractedPages']} | "
f"Start: {prog['startTime']} | End: {prog['endTime']}"
)
break
elif state == "failed":
print("❌ Job failed:", data.get("errorMsg", "Unknown error"))
sys.exit(1)
time.sleep(5)
# ========== 获取 OCR 结果 ==========
jsonl_url = data["resultUrl"]["jsonUrl"]
print(f"📥 Fetching result: {jsonl_url}")
resp = requests.get(jsonl_url)
resp.raise_for_status()
```
&emsp;&emsp;这里只处理了url形式的文档实际开发中可以搭建一个管理页面集中处理文档资料。
&emsp;&emsp;获取到的结果是按照区域划分的,还需要进一步的处理:
```python
pages = []
for line in resp.text.strip().splitlines():
if not line.strip():
continue
obj = json.loads(line)
for page in obj.get("result", {}).get("ocrResults", []):
pruned = page.get("prunedResult", {})
texts = pruned.get("rec_texts", [])
# 一页一段
page_text = "\n".join(texts)
pages.append(page_text)
# ✅ 最终文档
full_doc = "\n\n".join(pages)
```
## 2.2 向量化
&emsp;&emsp;使用在线或者离线向量化模型处理:
```python
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每个 chunk 最大长度
chunk_overlap=50, # 重叠,防止切坏语义
separators=["\n\n", "\n", "。", "", "", "", "", " ", ""]
)
chunks = text_splitter.split_text(full_doc)
print(f"✅ 切分成 {len(chunks)} 个 chunk")
for i, chunk in enumerate(chunks):
print(f"\n===== Chunk {i} =====")
print(chunk)
model = SentenceTransformer("BAAI/bge-base-zh", cache_folder="./models")
embeddings = model.encode(
chunks,
normalize_embeddings=True
)
print(f"✅ 向量维度: {embeddings.shape}")
client = chromadb.PersistentClient(path="./vector_db/train_docs")
collection = client.get_or_create_collection(name="train_docs")
for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
collection.add(
ids=[str(i)],
documents=[chunk],
embeddings=[emb.tolist()]
)
print("✅ 向量库写入完成")
```
&emsp;&emsp;这里使用的是`BAAI/bge-base-zh`模型,第一次会将模型下载到本地。
&emsp;&emsp;将处理结果存储到chroma向量数据库中。
## 2.3 检索系统
```python
model_path = "./models/models--BAAI--bge-base-zh/snapshots/0e5f83d4895db7955e4cb9ed37ab73f7ded339b6"
model = SentenceTransformer(model_path, local_files_only=True)
client = chromadb.PersistentClient(path="./vector_db/train_docs")
collection = client.get_or_create_collection(name="train_docs")
q = "培训计划怎么制定?"
q_emb = model.encode([q], normalize_embeddings=True)
res = collection.query(
query_embeddings=q_emb.tolist(),
n_results=3
)
print("\n\n".join(res["documents"][0]))
```
::: tip
注意这里使用离线模型时的路径
:::
## 2.4 结合大模型
&emsp;&emsp;常用提示词:
```python
你是专业的知识问答助手
请严格基于下方提供的参考资料回答问题
如果参考资料中不包含答案请明确说明当前资料无法回答该问题”。
不要编造推测或引入外部知识
参考资料
"""
{{context}}
"""
用户问题
{{question}}
请按以下要求回答
1. 回答简洁准确有条理
2. 必要时使用列表或分点说明
3. 避免冗余描述
```

103
docs/Web/AI/TextToSql.md Normal file
View File

@@ -0,0 +1,103 @@
---
title: Text To SQL开发
date: 2026-06-09
---
# 一、工作流程
```text
用户自然语言问题
【大模型】生成 SQL
【数据库】执行 SQL → 得到结构化结果
【大模型】分析 / 解读 / 总结 / 可视化建议
最终返回给用户(文字、结论、图表说明)
```
# 二、实战
## 2.1 获取数据库结构
```python
db = SQLDatabase.from_uri(os.getenv("DB_URL"), include_tables=['table_1', 'table_2'])
table_info = db.get_table_info()
```
::: warning
这里的数据库账号只能为只读权限。
:::
## 2.2 SQL提示词
```python
from langchain_core.prompts import PromptTemplate
SQL_PROMPT = PromptTemplate.from_template(
"""
你是一个 MySQL 专家。
请根据用户问题生成一条可执行的 MySQL 查询语句。
只返回 SQL不要解释不要加 ```。
数据库结构:
{schema}
数据库补充:
{option}
用户问题:
{question}
"""
)
```
&emsp;&emsp;数据库结构就是刚才生成的`table_info`,数据库补充就是对一些关系表的说明和字段含义的解释等。
## 2.3 生成SQL
```python
sql = chat_model.invoke(
SQL_PROMPT.format(schema=table_info, option=option, question=request.message)
).content.strip()
```
&emsp;&emsp;这里的`chat_model``init_chat_model`构建的大模型。
## 2.4 执行SQL
```python
from sqlalchemy import create_engine
from sqlalchemy import text
engine = create_engine(
os.getenv("DB_URL"),
pool_pre_ping=True,
)
conn = engine.connect()
def is_select(sql: str) -> bool:
return sql.lower().lstrip().startswith("select")
def execute_sql(sql: str):
if not is_select(sql):
raise ValueError("只允许 SELECT 查询")
result = conn.execute(text(sql))
rows = result.fetchall()
columns = result.keys()
return {
"columns": list(columns),
"rows": rows
}
```
&emsp;&emsp;使用sqlalchemy执行SQL并加入了安全校验。
## 2.5 分析数据
```python
agent = create_agent(
model=chat_model,
system_prompt=SYSTEM_PROMPT.format(question=request.message, result=result),
)
response = agent.invoke({"message": HumanMessage(content=request.message)})
```
&emsp;&emsp;这里的系统提示词根据情况编写。

77
docs/Web/AI/Tools.md Normal file
View File

@@ -0,0 +1,77 @@
---
title: Tools开发
date: 2026-06-29
---
# 一、工作流程
```text
【定义工具能力】
【约定输入输出】
【实现工具逻辑】
【注册到模型】
用户自然语言请求
【模型选择工具】
【执行工具】
【返回结果】
```
# 二、实战
## 2.1 工具定义
&emsp;&emsp;实现一个根据接口查询数据的工具:
```python
@tool
def repair_search(date: list[str]) -> str:
"""
查询维修工单列表。
参数说明:
- date (list[str], 必填):
- 日期范围,格式:["YYYY-MM-DD", "YYYY-MM-DD"]
- 示例:["2026-07-01", "2026-08-01"]
"""
if date is None:
date = []
params = {
"currentPage": 0,
"pageSize": 0,
"customer": "",
"deviceName": "",
"deviceModel": "",
"date": date
}
token = get_token()
headers = {
"satoken": token
}
try:
resp = requests.get(f"{BASE_URL}repair", params=params, headers=headers, timeout=10)
resp.raise_for_status()
return resp.text
except Exception as e:
return f"查询维修工单失败: {e}"
```
## 2.2 注册到模型
```python
integrate_agent = create_agent(
model=model,
tools=[repair_search],
system_prompt=integrate_prompt
)
```
::: tip
工具里面的注释大模型是无法查看的,需要把工具的使用说明,例如接口返回值的含义,写在提示词里面。
:::

View File

@@ -0,0 +1,33 @@
---
title: FastAPI Docker离线部署
date: 2026-01-06
---
# 一、安装依赖
&emsp;&emsp;通过pip将requirements.txt中的依赖包下载到本地
```sh
pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple
```
::: tip
`--only-binary=:all:`表示只下载预编译的二进制包
`--platform manylinux2014_x86_64`指定目标平台为 Linux
`-i`指定pip镜像源源
还可以加上`--python-version 310`指定python版本
:::
&emsp;&emsp;执行命令后,会在./packages中下载相应的二进制包。
# 二、Dockfile
```dockerfile
FROM python:3.10-slim
WORKDIR /app
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
COPY ./packages /app/packages
COPY requirements.txt /app/
RUN pip install --no-cache-dir --no-index --find-links=/app/packages -r requirements.txt
COPY . /app/
EXPOSE 8095
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8095"]
```

View File

@@ -0,0 +1,29 @@
---
title: Flutter简介与安装
date: 2026-05-02
---
# 一、简介
&emsp;&emsp;Flutter 是 Google 推出的跨平台 UI 开发框架,使用 Dart 语言,通过自带的渲染引擎直接绘制界面,可一套代码同时构建 iOS、Android、Web 及 Windows/macOS/Linux 桌面应用,具备原生级性能、热重载和高 UI 一致性,适合追求多端统一与体验效率的项目开发。
# 二、安装
## 2.1 下载SDK
&emsp;&emsp;打开[官网](https://docs.flutter.dev/install/archive)选择SDK版本下载然后解压到相应目录。
## 2.2 环境变量配置
1. 在系统变量->path中添加'/path/to/flutter/bin'
2. 在系统变量中添加: 'PUB_HOSTED_URL'->'https://pub.flutter-io.cn'
3. 在系统变量中添加: 'FLUTTER_STORAGE_BASE_URL'->'https://storage.flutter-io.cn'
4. 在系统变量中添加: 'FLUTTER_GIT_URL'->'https://gitee.com/mirrors/Flutter.git'
## 2.3 校验安装
&emsp;&emsp;输入`flutter --version``flutter doctor`查看是否有错误输出。
::: tip
Windows开发需要安装Visual Studio的'C++的桌面开发'。
Android开发需要安装Android SDK。
Web开发需要安装Chrome浏览器。
:::
## 三、Android Studio
1. 安装Flutter和Dart插件
2. 在Settings->Languages->Dart中选择Dart路径`/path/to/flutter/bin/cache/dart-sdk`

View File

@@ -262,10 +262,47 @@ public interface UserMapper {
</resultMap>
```
&emsp;&emsp;其中`<collection>`也可以使用嵌套查询:
&emsp;&emsp;对应的数据结构为:
```java
public class Blog {
private Integer id;
private String title;
private Author author; // 一对一
private List<Post> posts; // 一对多
}
public class Author {
private Integer id;
private String username;
private String password;
private String email;
private String bio;
private String favouriteSection;
}
public class Post {
private Integer id;
private String subject;
private Author author; // 一对一
private List<Comment> comments; // 一对多
private List<Tag> tags; // 一对多
}
public class Comment {
private Integer id;
}
public class Tag {
private Integer id;
}
```
&emsp;&emsp;其中`<collection>`也可以使用嵌套查询,例如`<collection property="posts" ofType="Post" select="queryPost"/>`
```xml
<collection property="posts" ofType="Post" select="queryPost"/>
<select id="queryPost" resultMap="postResultMap">
</select>
<resultMap id="postResultMap" type="Post">
<id property="id" column="post_id"/>
@@ -281,11 +318,6 @@ public interface UserMapper {
<case value="1" resultType="DraftPost"/>
</discriminator>
</resultMap>
<select id="queryPost" resultMap="postResultMap">
</select>
```
&emsp;&emsp;如果需要传递参数,可以在`<collection>`添加`column`属性:
@@ -304,7 +336,12 @@ public interface UserMapper {
+ 映射语句文件中的所有 select 语句的结果将会被缓存。
+ 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
+ 一级缓存和二级缓存区别在于一级缓存只针对一次SqlSession二级缓存针对全局范围。
+ 一级缓存和二级缓存区别在于一级缓存只针对一次SqlSession二级缓存针对全局范围。
::: tip
一级缓存作用于service里面的同一个方法一个方法就是一个SqlSession多次查询时直接返回缓存数据如果是同一个service里面的不同方法就是多个SqlSession一级缓存会失效。
二级缓存作用于同一个mapper即使是不同的service的不同方法只要是同一个mapper都可以使用二级缓存。
:::
## 4.6 动态SQL
+ if :是/否
@@ -456,3 +493,70 @@ public class BlobToBase64TypeHandler extends BaseTypeHandler<String> {
}
}
```
&emsp;&emsp;例如自定义一个处理敏感字段(如密码、手机号)的自动加解密:
```java
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, AESUtil.encrypt(parameter)); // 自定义加密方法
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String encrypted = rs.getString(columnName);
return AESUtil.decrypt(encrypted); // 自定义解密方法
}
}
```
&emsp;&emsp;然后在实体类字段上加上 @TableField(typeHandler = EncryptTypeHandler.class)
```java
@TableName("user")
public class User {
private Long id;
private String name;
// 这个字段在数据库里存的是加密后的字符串
@TableField(typeHandler = EncryptTypeHandler.class)
private String phone; // 例如 "13800138000" → 存为 "U2FsdGVkX1+oO1W..."
@TableField(typeHandler = EncryptTypeHandler.class)
private String idCard; // 身份证号
// 普通字段,不加密
private String email;
}
```
&emsp;&emsp;如果是在XML文件中查询需要指定TypeHandler
```xml
<resultMap id="UserResultMap" type="com.example.entity.User">
<id column="id" property="id" />
<result column="name" property="name" />
<!-- 关键phone 字段使用 EncryptTypeHandler -->
<result column="phone" property="phone" typeHandler="com.example.handler.EncryptTypeHandler"/>
<!-- idCard 字段也用同一个处理器 -->
<result column="id_card" property="idCard" typeHandler="com.example.handler.EncryptTypeHandler"/>
<result column="email" property="email" />
</resultMap>
```
&emsp;&emsp;MyBatisPlus中定义了一些常用的类型处理器例如JacksonTypeHandler
```java
@TableField(value = "fault_type", typeHandler = JacksonTypeHandler.class)
```
&emsp;&emsp;在XML文件中将typeHandler设置为`com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler`
# 六、MyBatis/MyBatis Plus常见问题
1. 更新值为null的字段时会失效
&emsp;&emsp;需要更改字段的更新策略:
```java
@TableField(value = "file_name", updateStrategy = FieldStrategy.ALWAYS)
```
2. 自动填充出现失效
&emsp;&emsp;strictInsert/UpdateFill的默认填充策略是如果实体类中需要自动填充的字段已经有值了那么当前值就不会进行填充或者你想给一个字段填充null值也是不可以的。可以替换为setFieldValByName将其直接覆盖。一般是更新的时候需要替换新增默认就是没有值的

View File

@@ -0,0 +1,41 @@
---
title: SQL高阶用法
date: 2025-12-21
---
# 一、WITH
## 1.1 定义
&emsp;&emsp;SQL 中的WITH子句也被称为**公用表表达式**CTECommon Table Expression它的作用是**在执行主查询之前,先定义一个临时的结果集,这个结果集可以在后续的查询中被多次引用**,就像一个临时表一样。它能让复杂的 SQL 查询变得更清晰、更易读,还能简化嵌套查询的逻辑。
## 1.2 使用
```sql
-- 定义第一个CTE数学高分学生
WITH math_high_score AS (
SELECT name, score FROM student_score WHERE subject = '数学' AND score > 85
),
-- 定义第二个CTE语文高分学生
chinese_high_score AS (
SELECT name, score FROM student_score WHERE subject = '语文' AND score > 85
)
-- 主查询:查询既在数学高分又在语文高分的学生
SELECT m.name
FROM math_high_score m
JOIN chinese_high_score c ON m.name = c.name;
```
&emsp;&emsp;递归 CTE
```sql
WITH recursive dept_hierarchy AS (
-- 锚点成员查询顶级部门parent_id为NULL
SELECT dept_id, dept_name, parent_id, 1 AS level
FROM department
WHERE parent_id IS NULL
UNION ALL
-- 递归成员查询子部门关联自身的dept_id和parent_id
SELECT d.dept_id, d.dept_name, d.parent_id, dh.level + 1 AS level
FROM department d
JOIN dept_hierarchy dh ON d.parent_id = dh.dept_id
)
-- 主查询:获取所有部门的层级
SELECT * FROM dept_hierarchy;
```

View File

@@ -3,10 +3,9 @@
date: 2025-11-26
---
# MySQL知识
## 一、基础知识
### 1.1 数据类型
#### 1.1.1 汇总
# 一、基础知识
## 1.1 数据类型
### 1.1.1 汇总
|类型|存储空间|范围|适用场景|
| :-------------------------------------------------------------------------------------------------: | :-----------------: | :--------------------------: | :-----------------------: |
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">TINYINT</span>|1字节|-128 \~ 127|状态码、年龄、布尔值|
@@ -34,15 +33,15 @@
|MEDIUMBLOB|16MB||中等二进制对象|
|LONGBLOB|4GB||超大二进制对象|
#### 1.1.2 法则
### 1.1.2 法则
&emsp;&emsp;**整数选择**优先INT大数量用BIGINT布尔值用TINYINT(1)或BIT(1)
&emsp;&emsp;**小数选择**金融金额必须用DECIMAL非精确测量可用FLOAT/DOUBLE
&emsp;&emsp;**字符串选择**定长编码用CHAR变长文本用VARCHAR大文本用TEXT系列
&emsp;&emsp;**时间选择**日期用DATE精确时间用DATETIME自动更新用TIMESTAMP
&emsp;&emsp;**特殊场景**多选项用SET结构化数据用JSON
### 1.2 常用函数
#### 1.2.1 字符串函数
## 1.2 常用函数
### 1.2.1 字符串函数
1. CONCAT(str1, str2, ...)​​:连接字符串
```sql
SELECT CONCAT('Hello', ' ', 'World');
@@ -56,7 +55,7 @@ SELECT LENGTH('Hello');
SELECT SUBSTRING('Hello World', 7, 5); -- 返回 'World'
```
#### 1.2.2 日期/时间函数
### 1.2.2 日期/时间函数
1. NOW()返回当前日期和时间yyyy-MM-DD HH:mm:ss
2. CURDATE()返回当前日期不带时间部分yyyy-MM-DD
3. DATE_FORMAT(date, format):按照指定的格式返回日期值。
@@ -76,7 +75,7 @@ SELECT DATEDIFF('2024-12-31', '2024-01-01'); -- 返回364
SELECT STR_TO_DATE('01-09-2024', '%d-%m-%Y'); -- 返回 '2024-09-01'
```
#### 1.2.3 数值函数
### 1.2.3 数值函数
1. ROUND(x, d):将数值 x 四舍五入到 d 位小数。
```sql
SELECT ROUND(123.4567, 2); -- 返回 123.46
@@ -87,13 +86,13 @@ SELECT FLOOR(2.9); -- 返回 2
SELECT CEIL(2.1); -- 返回 3
```
#### 1.2.4 聚合函数
### 1.2.4 聚合函数
1. COUNT(expression):返回某列中的记录数。
2. SUM(expression):返回某列中数值的总和。
3. AVG(expression):返回某列中数值的平均值。
4. MAX(expression) / MIN(expression):返回某列的最大值或最小值。
#### 1.2.5 控制流函数
### 1.2.5 控制流函数
1. IF(condition, true_value, false_value):如果 condition 为真,返回 true_value否则返回 false_value。
```sql
SELECT IF(1 > 0, 'Yes', 'No'); -- 返回 'Yes'
@@ -109,11 +108,11 @@ SELECT
FROM employees;
```
## 二、索引
# 二、索引
&emsp;&emsp;索引是对数据库表中的一列或多列值进行**排序**的一种结构,使用索引可以快速访问数据库表中的特定信息。
&emsp;&emsp;**索引相当于图书上的目录,可以根据目录上的页码快速找到所需的内容,提高性能(查询速度)**
&emsp;&emsp;**索引相当于图书上的目录,可以根据目录上的页码快速找到所需的内容,提高性能(查询速度),相当于用空间换时间。**
### 2.1 优缺点
## 2.1 优缺点
优点:
* **查询速度起飞 (主要目的)** :通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。
* **保证数据唯一性**:通过创建**唯一索引 (Unique Index)** 可以确保表中的某一列或几列组合的值是独一无二的比如用户ID、邮箱等。**主键本身就是一种唯一索引**。
@@ -124,7 +123,7 @@ FROM employees;
* **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。
* **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
### 2.2 适用场景
## 2.2 适用场景
适用场景
* **频繁作为查询条件的字段**应该创建索引
* **查询中排序的字段**创建索引将大大提高排序的速度(索引就是排序加快速查找)
@@ -135,7 +134,7 @@ FROM employees;
* **表记录太少,不需要创建索引**
* **数据重复且分布平均的字段**,因此为经常查询的和经常排序的字段建立索引。注意某些数据包含大量重复数据,因此他建立索引就没有太大的效果,例如性别字段,只有男女,不适合建立索引。
### 2.3 数据结构
## 2.3 数据结构
&emsp;&emsp;在 MySQL 中MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构。
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/91d2e40a1dae728c4061d1e1845e0ae3.png 'MySQL索引.png')
&emsp;&emsp;例如要查找id=75的用户`SELECT * FROM users WHERE id = 75`
@@ -152,7 +151,7 @@ FROM employees;
|叶子节点连接|支持范围查询|ORDER BY、分页查询快|
|只在叶子存数据|内部节点小|更多索引数据放入内存|
### 2.4 索引类型
## 2.4 索引类型
1. 主键索引
主键索引是最特殊的索引。
```sql
@@ -178,7 +177,7 @@ CREATE INDEX idx_name ON users(name);
SELECT * FROM users WHERE name = "张三";
```
4. 合索引
4. 合索引
多个字段组合的索引​
```sql
CREATE INDEX idx_name_age_city ON users(name, age, city);
@@ -189,10 +188,22 @@ SELECT * FROM users WHERE name = '张三' AND age = 25 AND city = '北京';
SELECT * FROM users WHERE age = 25; -- 无法使用索引
SELECT * FROM users WHERE city = '北京'; -- 无法使用索引
```
&emsp;&emsp;合索引的使用最左前缀原则。
&emsp;&emsp;合索引的使用最左前缀原则。最左前缀原则指的是查询从索引的最左列开始不能跳过索引中的列。比如有A、B、C这3列作为索引则查询语句中必须是A、AB、AC、ABC不能是BC这样的跳过了ASQL语句中字段的位置可以任意但一定要包含比如CBA顺序也可以。
&emsp;&emsp;联合索引中,出现范围查询(<, >),范围查询右侧的列索引失效。可以用>=或者<=来规避索引失效问题。
&emsp;&emsp;`CREATE INDEX idx_name_age_city ON users(name, age, city);`相当于创建了3个索引`users(name)users(name, age)users(name, age, city)`​。
### 2.5 索引设计
::: tip
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:
| 分类 | 含义 | 特点 |
|---|---|---|
| 聚集索引 | 将数据存储与索引放在一起,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个 |
| 二级索引 | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键值 | 可以存在多个 |
&emsp;&emsp;聚集索引就是主键索引,其他的都是二级索引,不能存放所有的数据。
&emsp;&emsp;比如查询`select * from user where name = 'Arm';`时,虽然使用了索引,但是只能查到`Arm`对应的`user_id`主键值,还需要根据主键值去查找完整的行记录。
:::
## 2.5 索引设计
* 为WHERE条件添加索引
```sql
SELECT * FROM orders WHERE user_id = 123;
@@ -223,7 +234,7 @@ CREATE INDEX idx_city_age_create_time ON users(city, age, create_time);
* 被频繁更新的字段应该慎重建立索引,虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
* 尽可能的考虑建立联合索引而不是单列索引。因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
### 2.6 索引优化
## 2.6 索引优化
1. 监控慢查询
```sql
-- 开启慢查询日志
@@ -243,7 +254,7 @@ CREATE INDEX idx_title_prefix ON articles(title(20)); -- 只索引前20个字
## 三、事务
&emsp;&emsp;事务是数据库操作的基本单位,它是一组原子性的 SQL 语句,或者说是一个独立的工作单元。事务内的所有操作**要么全部成功,要么全部失败**。
### 3.1 特性
## 3.1 特性
* **原子性**`Atomicity`​):原子性确保事务中的所有操作要么全部完成,要么全部不完成。如果事务执行过程中发生错误,所有已执行的操作都会回滚。
```sql
START TRANSACTION;
@@ -286,13 +297,13 @@ COMMIT;
-- 提交后,数据已经持久化到磁盘
```
### 3.2 隔离级别
#### 3.2.1 概念
## 3.2 隔离级别
### 3.2.1 概念
* **脏读 (Dirty Read):** 事务A读取了事务B**尚未提交**的修改数据。如果事务B最终回滚事务A读到的就是无效的"脏"数据。
* **不可重复读 (Non-Repeatable Read):** 在同一个事务A中**两次读取同一行数据**得到的结果不同。这是因为在两次读取之间该行数据被另一个提交了的事务B**修改**了。
* **幻读 (Phantom Read):** 在同一个事务A中**两次执行相同的查询**(通常是范围查询 `SELECT ... WHERE ...`​),得到的结果集行数不同(出现了新的"幻影"行或原有行消失了。这是因为在两次查询之间另一个提交了的事务B**插入**了满足查询条件的新行或**删除**了原有的行。
#### 3.2.2 类型
### 3.2.2 类型
1. 读未提交 (READ UNCOMMITTED)
* 特点: 这是最低的隔离级别,允许读取尚未提交的数据变更。
* 允许的问题:
@@ -333,7 +344,7 @@ COMMIT;
* 数据一致性: **最好**。完全保证事务的隔离性,不会出现任何并发问题。
* 使用场景: 仅在对数据一致性要求极高,且完全不能接受任何并发副作用(如金融核心系统的某些极端操作),并且能承受极低并发性能的情况下使用。**实践中很少使用。**
### 3.3 MySQL锁
## 3.3 MySQL锁
* **表级锁:** MySQL 中锁定粒度最大的一种锁全局锁除外是针对非索引字段加的锁对当前操作的整张表加锁实现简单资源消耗也比较少加锁快不会出现死锁。不过触发锁冲突的概率最高高并发下效率极低。表级锁和存储引擎无关MyISAM 和 InnoDB 引擎都支持表级锁。
* **行级锁:** MySQL 中锁定粒度最小的一种锁,是 **针对索引字段加的锁** ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
* **共享锁S 锁)** :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
@@ -348,35 +359,36 @@ COMMIT;
|UPDATE|排他锁(X)|自动获取|
|DELETE|排他锁(X)|自动获取|
## 四、日志
# 四、日志
&emsp;&emsp;MySQL 的三大核心日志系统是保证数据一致性、实现故障恢复和提供复制功能的关键组件。这三大日志分别是:​​二进制日志(binlog)​​、​​错误日志(error log) 和 ​​重做日志(redo log)​​。
### 4.1 二进制日志Binary Log简称 binlog
#### 4.1.1 概念
## 4.1 二进制日志Binary Log简称 binlog
### 4.1.1 概念
&emsp;&emsp;二进制日志​​是 MySQL ​​记录所有修改数据或可能修改数据的语句(或数据变更)的日志文件​​。它记录了数据库执行的​​更改操作​​(如 INSERT、UPDATE、DELETE 等 DML 操作,以及 CREATE、ALTER、DROP 等 DDL 操作),但**不记录 SELECT 这类不修改数据的查询操作**。
#### 4.1.2 作用
### 4.1.2 作用
1. 主从复制Replication在主从架构中主库将 binlog 发送给从库,从库通过读取并重放 binlog 来保持与主库的数据同步。
2. 数据恢复Point-in-Time Recovery通过备份 + binlog 可以恢复到某个具体时间点。
3. ​​审计​​:可以追踪数据库的所有变更操作。
### 4.2 重做日志Redo Log
#### 4.2.1 概念
## 4.2 重做日志Redo Log
### 4.2.1 概念
&emsp;&emsp;重做日志​​是 InnoDB 存储引擎特有的日志,它记录的是 ​​“物理级别” 上的页修改信息​​,主要用于 崩溃恢复Crash Recovery
&emsp;&emsp;InnoDB 使用 WALWrite-Ahead Logging预写日志技术即 ​​事务提交时,先写 redo log再写磁盘数据页从而保证即使系统崩溃也能通过 redo log 恢复数据。
#### 4.2.2 作用
### 4.2.2 作用
1. 实现事务的持久性Durability确保事务提交后即使发生宕机数据也不会丢失。
2. ​​提高写入性能​​:数据不是每次修改都直接写磁盘,而是先写 redo log顺序写速度快后续再异步刷盘。
3. ​​支持 crash-safeMySQL 宕机重启后,可通过 redo log 恢复未刷盘的数据。
### 4.3 回滚日志Undo Log
#### 4.3.1 概念
## 4.3 回滚日志Undo Log
### 4.3.1 概念
&emsp;&emsp;回滚日志​​也是 InnoDB 引擎特有​​ 的日志,它记录的是 ​​数据被修改前的原始值​​
#### 4.3.2 作用
### 4.3.2 作用
1. 支持事务回滚​​:如果事务执行失败或调用了 ROLLBACK可以通过 undo log 将数据恢复到修改之前的状态。
2. 实现 MVCC多版本并发控制在读已提交RC、可重复读RR隔离级别下undo log 用于提供历史版本数据,使得不同事务能看到不同的数据快照,而不需要加锁。
### 4.4 和事务、锁之间的关系
## 4.4 和事务、锁之间的关系
&emsp;&emsp;例如当执行一条SQL语句时事务、锁、日志等之间的关系
1. 发起一个事务
2. 执行一系列的DML操作如 INSERT/UPDATE/DELETE
@@ -389,7 +401,7 @@ COMMIT;
&emsp;&emsp;**undo log用于实现事务回滚、MVCC。**
4. **背后有日志系统默默记录一切,锁系统保障并发安全,隔离级别定义了“你能看到啥”。​**
#### 4.4.1 相互关系
### 4.4.1 相互关系
1. 事务是锁的使用者,锁是事务实现隔离性的手段。​当一个事务对某行数据执行 UPDATE/DELETE 操作时为了防止其他事务同时修改相同数据InnoDB 会自动对该行或索引加 排他锁X锁。如果事务只是读取数据根据隔离级别可能会加 共享锁S锁 或使用 MVCC不加锁
2. 隔离级别定义了事务之间的可见性规则,是事务“隔离性”的具体体现。​事务的隔离性是通过锁 + MVCC多版本并发控制依赖 undo log+ 隔离级别共同实现的。​
3. 日志是事务实现 持久性、崩溃恢复、回滚 等能力的基石。redo log重做日志保障事务的持久性Durability事务提交时先将数据页的变更记录到 redo log顺序写高性能随后再异步刷盘到磁盘数据页。即使系统崩溃重启后也能通过 redo log 恢复已提交但尚未刷盘的数据。undo log回滚日志支持事务回滚 和 MVCC。事务修改数据前会先把原始数据保存到 undo log如果事务回滚可以用它恢复旧值。同时undo log 也是 MVCC多版本控制实现的基础用于提供历史版本数据。binlog二进制日志用于主从复制、时间点恢复。虽然 binlog 是 Server 层的日志,不属于 InnoDB 事务引擎的一部分但在事务提交时binlog 与 redo log 通过两阶段提交2PC保证一致性
@@ -397,14 +409,42 @@ COMMIT;
5. 锁保障并发安全,日志保障操作可恢复。两者从不同维度确保数据库的正确性。​锁​​:是在运行时控制谁可以访问哪些数据,是 ​​并发控制​​ 的手段。​​日志​​:是在磁盘上记录操作过程,是 ​​故障恢复 & 事务一致性​​ 的手段。
6. 隔离级别定义了事务间数据的可见性,而日志(尤其是 undo log 和 binlog为这种“可见性”提供了实现基础。undo log 是实现 MVCC多版本并发控制 的基础,而 MVCC 是 REPEATABLE READ 等隔离级别的关键。binlog 虽不直接影响隔离性,但它记录了事务的变更历史,是构建主从环境、实现数据恢复的基础。
## 五、优化
### 5.1 Explain 执行计划
&emsp;&emsp;`EXPLAIN`​是 MySQL 自带的一个诊断工具,它可以模拟 MySQL 查询优化器的执行过程,对`SELECT`​语句(在 MySQL 8.0 及以上版本,也支持对`UPDATE`​、`DELETE`​等语句使用)进行分析,并输出该语句的执行计划。
#### 5.1.1 基本用法
&emsp;&emsp;在select语句前面加上EXPLAIN关键字即可例如`EXPLAIN SELECT * FROM users WHERE age > 25;
`
# 五、优化
## 5.1 开启慢查询
&emsp;&emsp;查看慢查询是否开启:
```bash
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'slow_query_log_file';
```
#### 5.1.2 输出列说明
&emsp;&emsp;临时开启慢查询,重启服务后无效:
```bash
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
```
&emsp;&emsp;永久开启慢查询:
```bash
[mysqld]
slow_query_log = ON
long_query_time = 1
```
&emsp;&emsp;开启后可以通过查看`SHOW VARIABLES LIKE 'slow_query_log_file'`查看记录的文件路径。
## 5.2 查看SQL运行时间
```bash
SET profiling = 1;
SELECT name, age FROM users WHERE name = "user235909051";
SHOW PROFILES;
```
## 5.3 Explain 执行计划
&emsp;&emsp;`EXPLAIN`​是 MySQL 自带的一个诊断工具,它可以模拟 MySQL 查询优化器的执行过程,对`SELECT`​语句(在 MySQL 8.0 及以上版本,也支持对`UPDATE`​、`DELETE`​等语句使用)进行分析,并输出该语句的执行计划。
### 5.3.1 基本用法
&emsp;&emsp;在select语句前面加上EXPLAIN关键字即可例如`EXPLAIN SELECT * FROM users WHERE age > 25;`
### 5.3.2 输出列说明
| 列名 | 说明 | 示例 |
| :-: | :-: | :-: |
| id | 查询标识符 | 相同 id 表示同组查询,执行顺序从上到下;不同 id 值越大优先级越高 |
@@ -420,7 +460,7 @@ COMMIT;
| filtered | 存储引擎返回数据后在 server 层过滤的比例 | |
| **Extra** | 额外执行信息 | |
#### 5.1.3 重要指标Type
### 5.3.3 重要指标Type
| 类型 | 描述 | 性能 | 示例 |
| - | - | - | - |
| system | 系统表,仅一行 | 最优 | MyISAM 引擎的空表 |
@@ -436,7 +476,7 @@ COMMIT;
| index | 全索引扫描 | 差 | SELECT indexed_col FROM table |
| ALL | 全表扫描 | 最差 | 无索引查询|
#### 5.1.4 重要指标Extra
### 5.3.4 重要指标Extra
| 值 | 含义 | 优化建议 |
| - | - | - |
| Using index | 覆盖索引(无需回表) | 优,保持 |
@@ -449,13 +489,72 @@ COMMIT;
| Impossible WHERE | WHERE 条件永不成立 | 查询逻辑错误 |
| Distinct | 优化 DISTINCT 操作 | |
## 六、执行过程
# 六、执行过程
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/8ddaa246aef63b6be371487ad7159e40.png '执行过程.png')
&emsp;&emsp;MySQL架构分为两层Service层和存储引擎层。
&emsp;&emsp;MySQL架构分为两层Service层和存储引擎层。
&emsp;&emsp;Server 层负责建立连接、分析和执行 SQL。存储引擎层负责数据的存储和提取。
1. 连接器:建立连接、管理连接、校验用户身份。
2. 查询缓存MySQL8.0已删除)
3. 解析SQL通过解析器对SQL查询语句进行词法分析、语法分析然后构建语法树方便后续模块读取表名、字段和语句类型等。
4. 执行SQL分为预处理阶段、优化阶段和执行阶段。
# 七、视图
## 7.1 定义
&emsp;&emsp;视图View是 MySQL 中的一种虚拟表,它的数据来源于一个或多个实际的表(基表),**其结构和数据是通过 SQL 查询语句定义的**。简单来说,视图就像是一个 “查询窗口”,你看到的是基表的数据,但**视图本身并不存储实际数据,每次访问视图时都会执行对应的查询语句**。
## 7.2 作用
1. 简化复杂查询:将多表关联、聚合等复杂的 SQL 逻辑封装到视图中,后续使用时只需查询视图即可。
2. 数据安全:可以只暴露基表中的部分列或部分行给用户,隐藏敏感数据。
3. 数据一致性:如果业务逻辑发生变化,只需修改视图的定义,而无需修改所有使用该逻辑的查询。
## 7.3 使用
&emsp;&emsp;创建视图
```sql
-- 创建视图v_student_basic
CREATE VIEW v_student_basic
AS
SELECT name, age, gender FROM student;
```
&emsp;&emsp;使用视图和正常使用数据表一致:
```sql
-- 查询视图(和查询普通表的方式完全一样)
SELECT * FROM v_student_basic;
```
# 八、事件
## 8.1 定义
&emsp;&emsp;MySQL 事件Event也被称为事件调度器Event Scheduler`MySQL 中一种定时执行的数据库对象`,可以理解为数据库层面的 “定时任务” 或 “计划任务”。它能根据你设定的时间规则(一次性执行、周期性执行),自动触发并执行指定的 SQL 逻辑(如数据清理、统计报表生成、数据同步等)。
## 8.2 特点
1. 定时执行:支持一次性执行(如某个具体时间点)和周期性执行(如每天凌晨 3 点、每小时执行一次)。
2. 自动运行:依赖 MySQL 的事件调度器线程,只要调度器开启,事件就会按规则自动触发。
3. 与存储过程结合:事件的执行逻辑可以是单条 SQL也可以是复杂的存储过程推荐用存储过程封装复杂逻辑
## 8.3 作用
- 数据清理:定期删除过期数据(如删除 30 天前的日志表数据)。
- 数据统计:定时生成业务统计报表(如每天凌晨统计前一天的订单数据)。
- 数据同步:定期将 A 表的数据同步到 B 表。
- 定时备份:定期执行数据库备份脚本(配合存储过程)。
## 8.4 使用
&emsp;&emsp;开启事件调度器:
```sql
-- 查看事件调度器状态ON表示开启OFF表示关闭
SHOW VARIABLES LIKE 'event_scheduler';
-- 临时开启MySQL重启后会恢复为默认状态
SET GLOBAL event_scheduler = ON;
-- 永久开启需要修改my.cnf/my.ini配置文件添加以下内容然后重启MySQL
event_scheduler = ON # 放在[mysqld]节点下
```
&emsp;&emsp;每天凌晨 1 点执行的事件,删除日志表中 7 天前的过期数据:
```sql
-- 创建周期性事件每天凌晨1点执行无结束时间
CREATE EVENT IF NOT EXISTS event_clear_log
ON SCHEDULE EVERY 1 DAY STARTS '2025-12-22 01:00:00'
COMMENT '每天凌晨1点删除7天前的日志'
DO
DELETE FROM log WHERE create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);
```

56
docs/Web/Network/Mqtt.md Normal file
View File

@@ -0,0 +1,56 @@
---
title: Mqtt简介与使用
date: 2026-05-27
---
# 一、简介
&emsp;&emsp;MQTTMessage Queuing Telemetry Transport 是一种 轻量级物联网通信协议,基于 发布 / 订阅Pub/Sub 模式。
## 1.1 核心概念
1. Broker消息服务器
2. Client发布或订阅消息的设备 / 程序
3. Topic: 消息主题(路径形式)
4. Publish发布消息
5. Subscribe订阅消息
6. QoS服务质量等级0 / 1 / 2
## 1.2 工作方式
```text
[设备A] --发布--> TopicA --> [Broker] --> TopicA --> [设备B]
```
# 二、安装
&emsp;&emsp;[EMQX](https://www.emqx.com/zh) Enterprise 是基于开源 EMQX 扩展的企业级 MQTT 物联网消息接入平台,完全兼容 MQTT v3.1.1 / v5.0。
## 2.1 docker安装
```yml
services:
emqx:
image: emqx/emqx-enterprise:5.10.0
container_name: emqx
ports:
- "1883:1883"
- "8083:8083"
- "8084:8084"
- "8883:8883"
- "18083:18083"
restart: always
```
&emsp;&emsp;启动容器后输入ip:18083默认账号密码为admin/public。
# 三、消息规范
## 3.1 Topic
```text
<domain>/<product>/<device_id>/<msg_type>/<resource>
```
&emsp;&emsp;例如`iot/thermostat/dev001/status/online``iot/thermostat/dev001/cmd/set_mode`
## 通配符
1. +单层通配符例如iot/+/dev001/telemetry/+
2. #多层通配符只能放在topic末尾例如iot/thermostat/dev001/#
::: tip
使用+通配符时必须严格满足层级要求例如如果topic是5层那么iot/+将无法匹配到消息
:::

392
docs/Web/Network/NetCore.md Normal file
View File

@@ -0,0 +1,392 @@
---
title: .Net Core实现
date: 2026-05-27
---
# 一、.Net Core实现
## 1.1 SuperSocket
&emsp;&emsp;[SuperSocket](https://www.supersocket.net/)是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。可以轻松构建TCP、UDP、WebSocket服务器。
## 1.2 安装依赖
&emsp;&emsp;NuGut安装SuperSocket、SuperSocket.WebSocket和SuperSocket.WebSocket.Server 2.0及以上版本。
## 1.3 配置文件
&emsp;&emsp;appsettings.json
```json
{
"serverOptions": {
"TcpServer": {
"name": "TcpServer",
"listeners": [
{
"ip": "Any",
"port": 4040
}
]
},
"WebSocketServer": {
"name": "WebSocket",
"listeners": [
{
"ip": "Any",
"port": 5050
}
]
}
},
"Mqtt": {
"Host": "127.0.0.1",
"Port": 1883,
"ClientId": "MqttClient",
"Topics": [
"test/topic1",
"test/topic2"
]
}
}
```
## 1.4 主程序
&emsp;&emsp;program.cs
```cs
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddSingleton<IPackageHandler<ProtocolFrame>, TcpPackageHandler>();
services.AddSingleton<WebSocketMessageHandler>();
services.AddSingleton<WebSocketConnectionManager>();
// 1. 注入 MQTT 配置
services.Configure<MqttSettings>(context.Configuration.GetSection("Mqtt"));
// 2. 注册 MQTT 客户端
services.AddSingleton<IMqttClient>(serviceProvider => new MqttClientFactory().CreateMqttClient());
// 3. 注册 MQTT 后台服务
services.AddHostedService<MqttService>();
})
.AsMultipleServerHostBuilder()
.AddServer<TcpService, ProtocolFrame, BinaryPipelineFilter>(builder =>
{
builder.ConfigureServerOptions((ctx, config) => config.GetSection("TcpServer"));
})
.AddWebSocketServer(builder =>
{
builder
.UseSessionHandler(
session =>
{
if (session is WebSocketSession wsSession)
{
var connectionManager = session.Server.ServiceProvider.GetRequiredService<WebSocketConnectionManager>();
connectionManager.Add(wsSession);
}
return ValueTask.CompletedTask;
},
(session, reason) =>
{
if (session is WebSocketSession wsSession)
{
var connectionManager = session.Server.ServiceProvider.GetRequiredService<WebSocketConnectionManager>();
connectionManager.Remove(wsSession);
}
return ValueTask.CompletedTask;
}
)
.UseWebSocketMessageHandler(async (session, package) =>
{
using var scope = session.Server.ServiceProvider.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<WebSocketMessageHandler>();
await handler.HandleAsync(session, package);
})
.ConfigureServerOptions((ctx, config) => config.GetSection("WebSocketServer"));
})
.ConfigureLogging(logging => logging.ClearProviders())
.UseNLog()
.Build();
await host.RunAsync();
```
&emsp;&emsp;通过.AsMultipleServerHostBuilder()可以构造多服务器实例。
# 二、 TCP服务器
## 2.1 协议数据
```cs
/// <summary>
/// 协议数据结构 示例65 6D 00 05 00 01 68 65 6C 6C 6F
/// </summary>
/// <param name="Magic">帧头标识</param>
/// <param name="Length">数据长度</param>
/// <param name="Type">数据类型</param>
/// <param name="Payload">数据内容</param>
///
record ProtocolFrame(
ushort Magic,
ushort Length,
ushort Type,
byte[] Payload
);
```
&emsp;&emsp;可以根据实际情况自定义消息格式。
## 2.2 协议解析
```cs
class BinaryPipelineFilter : FixedHeaderPipelineFilter<ProtocolFrame>
{
/// <summary>
/// 固定头长度
/// </summary>
public BinaryPipelineFilter() : base(6)
{
}
/// <summary>
/// 从包头中解析出 Body 长度
/// </summary>
/// <param name="buffer">字节流</param>
/// <returns>Body 长度</returns>
protected override int GetBodyLengthFromHeader(ref ReadOnlySequence<byte> buffer)
{
var reader = new SequenceReader<byte>(buffer);
// 读取前2字节 → magic
reader.TryReadBigEndian(out ushort magic);
// 再读2字节 → Length
reader.TryReadBigEndian(out ushort length);
// 再读2字节 → Type
reader.TryReadBigEndian(out ushort type);
// 校验帧头
if (magic != 0x656D)
{
throw new Exception("非法帧头");
}
// 限制长度
if (length == 0 || length > 8192)
{
throw new Exception("非法长度");
}
// 返回 Payload 长度
return length;
}
/// <summary>
/// 把完整字节包 → 转换成 ProtocolFrame
/// </summary>
/// <param name="buffer">字节流</param>
/// <returns>ProtocolFrame</returns>
protected override ProtocolFrame DecodePackage(ref ReadOnlySequence<byte> buffer)
{
var reader = new SequenceReader<byte>(buffer);
// 读取前2字节 → magic
reader.TryReadBigEndian(out ushort magic);
// 再读2字节 → Length
reader.TryReadBigEndian(out ushort length);
// 再读2字节 → Type
reader.TryReadBigEndian(out ushort type);
var payload = buffer.Slice(6, length).ToArray();
// 构造ProtocolFrame
return new ProtocolFrame(magic, length, type, payload);
}
}
```
&emsp;&emsp;根据协议数据ProtocolFrame来解包。
## 2.3 消息处理
```cs
class TcpPackageHandler(ILogger<TcpPackageHandler> logger) : IPackageHandler<ProtocolFrame>
{
private readonly ILogger<TcpPackageHandler> _logger = logger;
public async ValueTask Handle(IAppSession session, ProtocolFrame package, CancellationToken cancellationToken)
{
_logger.LogInformation($"Magic={package.Magic:X4}, Type={package.Type}, Len={package.Length}");
await session.SendAsync(package.Payload, cancellationToken);
}
}
```
## 2.4 TCPService
```cs
class TcpService(IServiceProvider serviceProvider, IOptions<ServerOptions> serverOptions, ILogger<TcpService> logger) : SuperSocketService<ProtocolFrame>(serviceProvider, serverOptions)
{
private readonly ILogger<TcpService> _logger = logger;
private readonly ConcurrentDictionary<string, string> _sessionDevices = new();
public void BindDevice(string sessionId, string deviceNum)
{
_sessionDevices.AddOrUpdate(sessionId, deviceNum, (k, v) => deviceNum);
}
protected override async ValueTask OnSessionConnectedAsync(IAppSession session)
{
_logger.LogInformation("TCP Session 连上: {SessionID}", session.SessionID);
await base.OnSessionConnectedAsync(session);
}
protected override async ValueTask OnSessionClosedAsync(IAppSession session, CloseEventArgs e)
{
_logger.LogInformation("TCP Session 断开: {SessionID}, Reason={Reason}", session.SessionID, e.Reason);
if (_sessionDevices.TryRemove(session.SessionID, out var deviceNum))
{
_logger.LogInformation("设备 {DeviceNum} 标记离线", deviceNum);
}
await base.OnSessionClosedAsync(session, e);
}
}
```
# 三、Websocket服务器
## 3.1 消息处理
```cs
class WebSocketMessageHandler(ILogger<WebSocketMessageHandler> logger)
{
private readonly ILogger<WebSocketMessageHandler> _logger = logger;
public async ValueTask HandleAsync(WebSocketSession session, WebSocketPackage package)
{
_logger.LogInformation($"[WebSocket] {package.Message}");
await session.SendAsync("ok");
}
}
```
## 3.2 连接管理
```cs
public class WebSocketConnectionManager(ILogger<WebSocketConnectionManager> logger)
{
private readonly ConcurrentDictionary<string, WebSocketSession> _sessions = new();
private readonly ILogger<WebSocketConnectionManager> _logger = logger;
public void Add(WebSocketSession session) => _sessions.TryAdd(session.SessionID, session);
public void Remove(WebSocketSession session) => _sessions.TryRemove(session.SessionID, out _);
public async Task BroadcastAsync(string json)
{
var payload = new ReadOnlyMemory<byte>(System.Text.Encoding.UTF8.GetBytes(json));
var tasks = _sessions.Values
.Where(s => s.State == SessionState.Connected)
.Select(async s =>
{
try
{
await s.SendAsync(json);
}
catch
{
Remove(s);
}
});
await Task.WhenAll(tasks);
}
}
```
# 四、 MQTT服务器
## 4.1 MqttService
```cs
class MqttService(IMqttClient mqttClient, IOptions<MqttSettings> settings, ILogger<TcpPackageHandler> logger) : BackgroundService
{
private readonly IMqttClient _mqttClient = mqttClient;
private readonly MqttSettings _settings = settings.Value;
private readonly ILogger<TcpPackageHandler> _logger = logger;
private MqttClientOptions? _mqttOptions;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 构建连接配置
_mqttOptions = new MqttClientOptionsBuilder()
.WithClientId($"{_settings.ClientId}_{Guid.NewGuid():N}")
.WithTcpServer(_settings.Host, _settings.Port)
.WithCleanSession()
.Build();
_mqttClient.ConnectedAsync += OnConnectedAsync;
_mqttClient.DisconnectedAsync += OnDisconnectedAsync;
_mqttClient.ApplicationMessageReceivedAsync += HandleMessage;
_logger.LogInformation("[MQTT] 正在连接到服务器 {Host}:{Port}...", _settings.Host, _settings.Port);
await _mqttClient.ConnectAsync(_mqttOptions, stoppingToken);
// 等待程序停止
await Task.Delay(Timeout.Infinite, stoppingToken);
}
public async Task OnConnectedAsync(MqttClientConnectedEventArgs arg)
{
_logger.LogInformation("[MQTT] 已成功连接到服务器");
// 连接成功后订阅主题
foreach (var topic in _settings.Topics)
{
await _mqttClient.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce);
_logger.LogInformation($"[MQTT] 已订阅主题:{topic}");
}
await Task.CompletedTask;
}
public async Task OnDisconnectedAsync(MqttClientDisconnectedEventArgs arg)
{
_logger.LogError("[MQTT] 连接失败,原因:{Reason}", arg.Reason);
await Task.CompletedTask;
}
public async Task HandleMessage(MqttApplicationMessageReceivedEventArgs arg)
{
var topic = arg.ApplicationMessage.Topic;
var payload = Encoding.UTF8.GetString(arg.ApplicationMessage.Payload);
_logger.LogInformation($"\n[MQTT] 收到消息\n主题{topic}\n内容{payload}\n");
await Task.CompletedTask;
}
public async Task<bool> PublishAsync(string topic, string payload, CancellationToken cancellationToken = default)
{
try
{
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(payload)
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build();
await _mqttClient.PublishAsync(message, cancellationToken);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "[MQTT] 发布消息到主题 {Topic} 失败", topic);
return false;
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
await _mqttClient.DisconnectAsync();
await base.StopAsync(stoppingToken);
}
}
```
&emsp;&emsp;需要实现后台服务接口一直运行。

View File

@@ -433,45 +433,45 @@ class TcpMessageProcessor:
:::
# HTTP
## 定义
# 二、HTTP
## 2.1 定义
&emsp;&emsp;HTTPHyperText Transfer Protocol超文本传输协议是互联网的核心协议之一用于**客户端如浏览器、App与服务器之间的通信**是万维网WWW数据交换的基础。它定义了请求 / 响应的格式、传输规则和状态码等核心机制,支持从简单文本到复杂多媒体(图片、视频、文件)的传输,也是现代 Web 应用的底层通信标准。
## 特性
### 请求 - 响应模式
## 2.2 特性
### 2.2.1 请求 - 响应模式
&emsp;&emsp;通信由客户端主动发起请求服务器接收后处理并返回响应不存在服务器主动向客户端推送数据的情况HTTP/2 引入 Server Push 扩展,可主动推送关联资源)。
&emsp;&emsp;一次完整通信流程:**客户端建立连接 → 发送请求 → 服务器处理 → 返回响应 → 连接关闭**HTTP/1.1 默认开启长连接 Keep-Alive
### 无状态
### 2.2.2 无状态
&emsp;&emsp;**服务器不会保存客户端的会话状态**(如登录状态、浏览记录),每次请求都是独立的,服务器无法通过协议本身识别连续请求是否来自同一客户端。通过 Cookie、Session、Token如 JWT等机制补充状态管理。
## 版本
### HTTP/1.01996 年)
## 2.3 版本
### 2.3.1 HTTP/1.01996 年)
- 基础版本,支持 GET、POST、HEAD 三种请求方法。
- 每次请求都需要建立新的 TCP 连接(短连接),连接建立和关闭的开销大,性能较低。
- 不支持长连接、管线化请求,仅支持简单的文本传输。
### HTTP/1.11999 年)
### 2.3.2 HTTP/1.11999 年)
- 默认开启 长连接Keep-Alive同一 TCP 连接可处理多个请求,减少连接开销。
- 支持 管线化请求:客户端可连续发送多个请求,无需等待前一个响应返回(部分浏览器未完全支持)。
- 新增请求方法PUT、DELETE、OPTIONS、TRACE、CONNECT。
- 支持 chunked 编码分块传输、缓存控制Cache-Control、内容协商等核心功能。
### HTTP/22015 年)
### 2.3.3 HTTP/22015 年)
&emsp;&emsp;基于 SPDY 协议优化,核心目标是提升性能:
- 二进制帧传输:将请求 / 响应数据拆分为二进制帧,而非 HTTP/1.x 的文本格式,解析效率更高。
- 多路复用:同一 TCP 连接中可并发处理多个请求(通过帧的 Stream ID 区分),解决 HTTP/1.1 的 “队头阻塞” 问题。
- 服务器推送Server Push服务器可主动向客户端推送关联资源如 HTML 引用的 CSS/JS减少客户端请求次数。
- 头部压缩HPACK对请求头和响应头进行压缩减少传输体积HTTP/1.x 头部重复传输开销大)。
### HTTP/32022 年)
### 2.3.4 HTTP/32022 年)
&emsp;&emsp;基于 SPDY 协议优化,核心目标是提升性能:
- 解决 TCP 队头阻塞UDP 无连接特性,单个流的阻塞不影响其他流。
- 更快的连接建立QUIC 集成 TLS 1.3减少握手次数1-RTT 甚至 0-RTT 建立连接)。
- 更好的移动网络支持:支持连接迁移(如手机切换 WiFi/4G 时,连接不中断)。
## 组成
### 请求消息Request
## 2.4 组成
### 2.4.1 请求消息Request
&emsp;&emsp;客户端向服务器发送的请求格式,由 请求行、请求头、空行、请求体 四部分组成:
```http
GET /api/courses/1 HTTP/1.1 # 请求行
@@ -506,7 +506,7 @@ Content-Type: application/json
- 文件上传multipart/form-data如 LMS 系统的作业文件上传)。
- 纯文本text/plain。
### 响应消息Response
### 2.4.2 响应消息Response
&emsp;&emsp;服务器向客户端返回的响应格式,由 状态行、响应头、空行、响应体 四部分组成:
```http
HTTP/1.1 200 OK # 状态行
@@ -549,8 +549,8 @@ Set-Cookie: sessionId=abc123; Path=/
- 图片 / 视频:二进制流(如 image/jpeg、video/mp4
- 纯文本text/plain。
# WebSocket
## 定义
# 三、WebSocket
## 3.1 定义
&emsp;&emsp;WebSocket 是一种**全双工、双向、持久化的网络通信协议**(属于应用层协议),由 HTML5 规范定义,专门解决 HTTP 协议无法实现服务器主动向客户端推送数据的问题。
&emsp;&emsp;它的核心特点是:**一旦客户端与服务器建立连接,双方就可以在这个连接上实时、双向地发送数据**,无需像 HTTP 那样每次通信都由客户端发起请求,非常适合实时通信场景(如聊天、直播弹幕、实时数据监控、在线协作等)。
@@ -562,8 +562,8 @@ Set-Cookie: sessionId=abc123; Path=/
| 服务器主动推送 | 不支持HTTP/2 的 Server Push 仅能推送资源,非实时数据) | 原生支持,可主动向客户端发数据 |
| 协议标识 | http:// / https:// | ws:// / wss://(加密版)|
## 特性
### 握手过程
## 3.2 特性
### 3.2.1 握手过程
1. 客户端发送 HTTP 请求,请求头包含 Upgrade: websocket 和 Connection: Upgrade表示要升级为 WebSocket 协议);
2. 服务器响应 101 Switching Protocols握手成功连接转为 WebSocket 持久连接;
3. 后续通信不再使用 HTTP 格式,而是 WebSocket 帧格式(二进制 / 文本)。
@@ -587,50 +587,50 @@ Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 服务器加密后的密钥,客户端验证
```
### 全双工通信
### 3.2.2 全双工通信
&emsp;&emsp;此时 HTTP 连接已升级为 WebSocket 连接双方可以通过这个连接实时、双向地发送数据。数据传输采用帧Frame 格式,支持文本数据和二进制数据(如图片、视频流)。
### 帧格式
### 3.2.3 帧格式
&emsp;&emsp;WebSocket 数据以帧为单位传输,帧头包含操作码(文本帧 0x01、二进制帧 0x02、关闭帧 0x08 等)、掩码(客户端发送数据需掩码,服务器无需);
### 无同源限制
### 3.2.4 无同源限制
&emsp;&emsp;WebSocket 不遵循同源策略(但服务器可通过 Origin 头限制跨域);
### 心跳机制
### 3.2.5 心跳机制
&emsp;&emsp;通过 Ping/Pong 帧维持连接(避免网络设备断开空闲连接,如 LMS 系统需定期发送 Ping 帧,服务器回复 Pong 帧)。
# MQTT
## 定义
# 四、MQTT
## 4.1 定义
&emsp;&emsp;MQTTMessage Queuing Telemetry Transport消息队列遥测传输是一种**轻量级、低带宽、低功耗的发布 / 订阅Publish/Subscribe模式物联网IoT通信协议**,由 IBM 于 1999 年设计,核心目标是解决受限设备(如传感器、嵌入式设备)和低带宽、不稳定网络环境下的高效数据传输问题。
## 架构
## 4.2 架构
- 发布者Publisher发送消息的设备 / 服务;
- 订阅者Subscriber接收消息的设备 / 服务;
- broker代理服务器核心中间件接收发布者的消息根据「主题Topic」转发给订阅者如 EMQ X、Mosquitto、RabbitMQ 支持 MQTT 插件);
- 主题Topic消息的分类标识。
## 特性
### QoS服务质量等级
## 4.3 特性
### 4.3.1 QoS服务质量等级
- QoS 0最多一次消息发送一次不保证送达
- QoS 1至少一次消息至少送达一次可能重复
- QoS 2恰好一次消息仅送达一次最可靠。
### 轻量级
### 4.3.2 轻量级
&emsp;&emsp;头部开销极小(固定头部仅 2 字节),消息体支持二进制 / 文本,适合低带宽场景
### 保留消息Retained Message
### 4.3.3 保留消息Retained Message
&emsp;&emsp;broker 保存某个主题的最后一条消息,新订阅者订阅后立即收到该消息
### 遗嘱消息Will Message
### 4.3.4 遗嘱消息Will Message
&emsp;&emsp;客户端异常断开时broker 自动发送预设消息
### 清洁会话Clean Session
### 4.3.5 清洁会话Clean Session
&emsp;&emsp;客户端断开连接后broker 是否保留订阅信息和未送达消息
## 组成
## 4.4 组成
&emsp;&emsp;MQTT 消息由 固定头部Fixed Header、可变头部Variable Header、负载Payload 三部分组成:
| 部分 | 作用 |
|--------------|----------------------------------------------------------------------|
| 固定头部 | 必选2 字节起包含消息类型如发布、订阅、QoS 等级、是否保留消息等标识。 |
| 可变头部 | 可选,仅部分消息类型(如发布、订阅)需要,包含主题名、消息 ID 等信息。 |
| 负载 | 可选,消息的实际内容(如 JSON 字符串、二进制数据),例如 {"temperature": 25}。 |
| 负载 | 可选,消息的实际内容(如 JSON 字符串、二进制数据),例如 {"temperature": 25}。 |

View File

@@ -28,8 +28,8 @@ services:
# 三、挂载
## 3.1 挂载本地存储
&emsp;&emsp;打开控制台,选择存储->添加->本地存储,设置挂载路径(自定义,如`/local`),选择根文件夹路径为`/srv`,点击添加。
&emsp;&emsp;样就可以将`/local`路径映射到宿主机`/srv`目录了。
&emsp;&emsp;打开控制台,选择存储->添加->本地存储,设置挂载路径(自定义,如`/local`),选择根文件夹路径为`/srv`,点击添加。
&emsp;&emsp;里的`/srv`文件夹为`docker`里面的文件夹路径,一般只需要更改`docker compose`文件里面的`/srv:/srv`前面的`srv`路径为宿主机路径即可,后面`/srv`路径不要更改。
&emsp;&emsp;可以在宿主机的`/srv`目录中添加文件查看AList的主页是否可以正确显示。
# 四、API
@@ -71,4 +71,9 @@ await axios({
&emsp;&emsp;在AList的文件路径中加上前缀`/p/`。例如文件路径为`/local/Test/test.mp4`,则下载路径为`/p/local/Test/test.mp4`
::: tip
以上为相对路径使用时加上相应的ip和端口号。
:::
::: warning
如果出现无法访问的情况,需要在设置->全局里面,将`签名所有`关闭。
签名所有Alist是否将为这个存储下的所有文件请求生成带签名的临时链接如果开启则需要带上校验码签名否则不需要。
:::

View File

@@ -0,0 +1,154 @@
---
title: 图片处理服务
date: 2026-06-04
---
# 一、简介
&emsp;&emsp;基于 Node.js + Sharp 实现的轻量级图片处理服务。
&emsp;&emsp;[sharp](https://sharp.pixelplumbing.com/)是 Node.js 下非常常用的高性能图像处理库,主要用来缩放、裁剪、旋转、格式转换、压缩图片等,速度快、内存占用低。
&emsp;&emsp;支持路径+参数的形式实时压缩图片,例如`ip:port/a.png?q=80&w=1024&h=768`
# 二、核心方法
## 2.1 根据参数调整图片
```JavaScript
const sharp = require('sharp');
// 压缩配置
const compressOptions = {
quality: 0.7,
maxHeight: 800,
maxWidth: 600
};
/**
* 处理图片压缩和调整大小
* @param {string} imagePath 图片路径
* @param {Object} query 查询参数
* @returns {Promise<Buffer>} 处理后的图片Buffer
*/
async function processImage(imagePath, query) {
let sharpInstance = sharp(imagePath).rotate();
// 处理宽高参数
let targetWidth = parseInt(query.w) || parseInt(query.width);
let targetHeight = parseInt(query.h) || parseInt(query.height);
// 如果没有指定宽高,使用配置的默认最大宽高
if (!targetWidth && !targetHeight) {
targetWidth = compressOptions.maxWidth;
targetHeight = compressOptions.maxHeight;
}
// 如果指定了宽高,进行调整大小处理
if (targetWidth || targetHeight) {
const resizeOptions = {
width: targetWidth,
height: targetHeight,
fit: sharp.fit.inside, // 保持宽高比,图片会完整显示在指定区域内
withoutEnlargement: true // 不放大比原始尺寸小的图片
};
sharpInstance = sharpInstance.resize(resizeOptions);
}
// 处理质量参数(优先使用查询参数,否则使用配置的默认值)
const quality = query.q ? Math.max(1, Math.min(100, parseInt(query.q))) :
Math.round(compressOptions.quality * 100);
const finalQuality = Math.max(1, Math.min(100, quality));
// 转换为JPEG格式并压缩
return sharpInstance.jpeg({quality: finalQuality, mozjpeg: true}).toBuffer();
}
```
&emsp;&emsp;核心方法:[resize](https://sharp.nodejs.cn/api-resize/)、[jpeg](https://sharp.nodejs.cn/api-output/#jpeg)、[toBuffer](https://sharp.nodejs.cn/api-output/#tobuffer)。
## 2.2 返回图片数据
```JavaScript
/**
* 发送响应
* @param {ServerResponse} res 响应对象
* @param {number} statusCode 状态码
* @param {string} message 信息
*/
function sendResponse(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(message);
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'GET') {
return sendResponse(res, 405, '仅支持GET请求');
}
try {
const parsedUrl = url.parse(req.url, true);
const requestPath = parsedUrl.pathname;
const query = parsedUrl.query;
// 过滤根路径请求(避免直接访问服务根目录)
if (requestPath === '/') {
return sendResponse(res, 200, '图片处理服务已启动!');
}
const requestedFilePath = requestPath.slice(1);
const fullImagePath = path.join(IMAGE_BASE_DIR, requestedFilePath);
// 安全检查:防止路径遍历
if (!isSafePath(fullImagePath, IMAGE_BASE_DIR)) {
return sendResponse(res, 403, '访问拒绝:非法路径(路径遍历攻击)');
}
if (!fs.existsSync(fullImagePath)) {
return sendResponse(res, 404, `图片不存在:${requestedFilePath}`);
}
// 如果没有压缩参数,直接返回原图
if (!query.q && !query.w && !query.width && !query.h && !query.height) {
const fileStream = fs.createReadStream(fullImagePath);
const ext = path.extname(fullImagePath).slice(1);
res.writeHead(200, {'Content-Type': `image/${ext}`});
// 流式传输文件(适合大文件)
fileStream.pipe(res);
fileStream.on('error', (err) => {
sendResponse(res, 500, `读取文件失败:${err.message}`);
});
return;
}
// 处理图片压缩和调整大小
const imageBuffer = await processImage(fullImagePath, query);
res.writeHead(200, {
'Content-Type': 'image/jpeg',
'Content-Length': imageBuffer.length,
'Cache-Control': 'public, max-age=86400' // 缓存1天
});
res.end(imageBuffer);
} catch (error) {
console.error('服务器异常:', error);
sendResponse(res, 500, `服务器内部错误:${error.message}`);
}
});
```
&emsp;&emsp;原生Node.js中res用法
```js
res.writeHead(statusCode, [statusMessage], [headers]);
res.end(message)
```
::: tip
Express中req请求对象常用用法
1. 获取请求路径参数:`/user/:id`->`req.params.id`
2. 获取查询参数(?key=value`/search?name=tom&age=18`->`req.query.name`
3. 获取请求体:`req.body.username`
4. 获取请求头:`req.headers['user-agent']`
Express中res响应对象常用用法
1. 返回普通文本:`res.send('Hello Express');`
2. 返回JSON`res.json({ success: true, data: {} });`
3. 设置状态码:`res.status(404).json({ msg: 'Not Found' });`
4. 设置响应头:`res.set('Content-Type', 'text/plain');`
5. 结束响应:`res.end()`
:::

View File

@@ -0,0 +1,82 @@
---
title: InfluxDB简介和使用
date: 2026-05-11
---
# 一、简介
&emsp;&emsp;InfluxDB 是一个开源的时序数据库Time Series Database, TSDB由 InfluxData 公司开发专门用于存储、查询和处理时间序列数据按时间顺序记录的数据点。它在监控、物联网IoT、实时分析等场景中广泛应用是时序数据领域的流行解决方案之一。
&emsp;&emsp;InfluxDB 3.0 是一次底层的彻底重构(内核代号 IOx核心代码从 Go 改写为 Rust并从自研的 TSM 引擎转向基于 Apache Arrow 生态的开放架构。
# 二、特点
1. Rust 全新重写:更快、更省内存、更稳
2. 支持无限标签基数:不再怕设备 ID、用户 ID 这类高基数场景
3. 原生支持标准 SQL不用只学 InfluxQL普通 SQL 直接查
4. 底层基于 Arrow + Parquet列式存储、压缩率高、查询快
5. 原生对接对象存储:支持 S3/OSS冷热数据自动分层可存海量历史数据
# 三、概念
1. 库 Database相当于 MySQL 的库,隔离业务数据
2. 测量 Measurement相当于 MySQL 的表(比如 cpu、温度传感器
3. 标签 Tag维度、索引字段字符串如设备 id、地区用来筛选分组
4. 字段 Field实际指标值数值如温度、CPU 使用率,不做索引)
5. 时间 Time自带时间戳时序数据唯一主键、按时间排序
# 四、安装
## 4.1 Docker安装
```yml
services:
influxdb3-core:
image: influxdb:3.9.1-core
container_name: influxdb3-core
restart: unless-stopped
networks:
- influx-network
ports:
- "8181:8181"
volumes:
- ./influxdb3/data:/var/lib/influxdb3/data
- ./influxdb3/plugins:/var/lib/influxdb3/plugins
user: root
environment:
- TZ=Asia/Shanghai
command: >
influxdb3 serve
--node-id=node0
--object-store=file
--data-dir=/var/lib/influxdb3/data
--plugin-dir=/var/lib/influxdb3/plugins
influxdb3-explorer:
image: influxdata/influxdb3-ui:1.6.3
container_name: influxdb3-explorer
restart: unless-stopped
networks:
- influx-network
ports:
- "8600:80"
- "8888:8888"
volumes:
- ./config:/app-root/config:ro
- ./db:/db:rw
- ./ssl:/etc/nginx/ssl:ro
environment:
- TZ=Asia/Shanghai
command: --mode=admin
networks:
influx-network:
driver: bridge
```
## 4.2 token生成
&emsp;&emsp;进入influxdb3-core容器输入命令获取token
```bash
influxdb3 create token --admin
```
## 4.3 可视化页面
&emsp;&emsp;打开ip:8600具体端口看docker的配置选择`Configure->Servers`,点击`Connect Your First Server`输入InfluxDB服务器信息进行配置。
&emsp;&emsp;`Query Data->Data Explorer`中查看数据。
# 五、使用
## 5.1 SpringBoot使用

View File

@@ -0,0 +1,181 @@
---
title: Meilisearch简介和使用
date: 2026-08-15
---
# 一、简介
&emsp;&emsp;[Meilisearch](https://meilisearch.com.cn/docs/home) 是一个基于 Rust 构建的开源全文搜索引擎,面向“用户侧搜索体验”设计。通过 REST API 接收 JSON 文档并建立倒排索引,提供 sub50ms 的搜索响应、默认拼写容错、前缀搜索、相关度排序与过滤分面能力,适用于站内搜索、商品检索、文档/知识库搜索、Cmd+K、RAG 检索层等场景。
# 二、核心概念
| 概念 | 说明 |
|---|---|
| **Index索引** | 一组共享设置的文档集合,等价于“表”,如 `products``articles` |
| **Document文档** | JSON 对象,是搜索基本单位,必须包含主键 |
| **Primary Key主键** | 文档唯一标识,默认推断 `id`相同主键写入即覆盖upsert |
| **Task任务** | 所有写操作异步执行,返回 `taskUid`状态enqueued → processing → succeeded / failed |
| **Settings** | Index 级配置searchable / filterable / sortable / displayed attributes、ranking rules、synonyms、stop words、tokenizer |
| **Ranking Rules** | 默认相关度规则:`words → typo → proximity → attribute → sort → exactness`,可定制 |
| **Master Key** | 实例级最高密钥≥16 bytes用于生成其他 Key**禁止下发前端** |
| **API Key** | Search / Admin / Read-Only Admin / Chat按 action + index 粒度授权 |
| **Tenant Token** | 由 API Key 派生,搜索时注入 filter实现多租户行级隔离 |
| **Embedder** | 内建向量化配置,支持 OpenAI / HuggingFace / Ollama / Cohere / 自定义 REST用于 Hybrid Search |
# 三、与Elasticsearch比较
| 维度 | Meilisearch | Elasticsearch |
|---|---|---|
| 核心引擎 | Rust + LMDBmmap单机 ACID | Java + Apache Lucenedistributed |
| 部署形态 | 单进程 / 单容器 | 多节点集群shard + replica + 选主) |
| 查询延迟 | **sub-50ms 默认**(前缀/typo 开箱) | sub-100ms200ms需调优达到极低 P99 |
| 索引吞吐 | 中(异步 task 队列,单机受限) | 高bulk + 多 shard 并行) |
| 近实时性 | 亚秒级 task 完成即可见 | refresh_interval 默认 1s |
| 内存占用 | ~200MB 空闲,无 GC | 12GB+ 起JVM heap 调优关键 |
| 横向扩展 | 社区版单节点Cloud/企业版支持 shard+replica | 原生分片、副本、跨集群复制 |
| 规模上限 | 单机百万~千万文档(索引通常 < 100GB | PB 百亿~千亿文档 |
| 聚合分析 | facet 计数级 | 完整 aggregation / pipeline / SIEM |
| 分词/相关度 | 内置 Charabia + 默认 ranking | 显式 analyzer / DSL / fuzziness 调优 |
| 运维复杂度 | 启容器+key+dump | heap/shard/ILM/TLS/RBAC|
| 成本模型 | 单机 $1020/月级 | 托管最小集群 $150+/月级 |
| 许可证 | MIT CE / BUSL-1.1 | AGPL / SSPL / ELv2 |
# 四、Docker安装
```yml
services:
meilisearch:
image: getmeili/meilisearch:v1.16
container_name: meilisearch
restart: unless-stopped
ports:
- "7700:7700"
environment:
MEILI_MASTER_KEY: key
volumes:
- ./meili_data:/meili_data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
networks:
- meili-net
meilisearch-ui:
image: eyeix/meilisearch-ui:v0.15.1-lite
container_name: meilisearch-ui
restart: unless-stopped
ports:
- "24900:24900"
environment:
TZ: Asia/Shanghai
depends_on:
meilisearch:
condition: service_healthy
networks:
- meili-net
networks:
meili-net:
driver: bridge
```
&emsp;&emsp;启动后在浏览器中打开ip:24900新增实例和索引
# 五、SpringBoot集成
## 5.1 安装依赖
```xml
<dependency>
<groupId>com.meilisearch.sdk</groupId>
<artifactId>meilisearch-java</artifactId>
<version>0.20.0</version>
</dependency>
```
::: warning
该版本需要okhttp 4.12版本如有冲突需要指定okhttp版本
```xml
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
```
:::
## 5.2 配置属性
```yml
meilisearch:
host: http:/ip:7700
api-key: key
```
## 5.3 配置类
```java
@Configuration
@ConfigurationProperties(prefix = "meilisearch")
@Data
public class MeiliSearchConfig {
private String host;
private String apiKey;
@Bean
public Client meiliClient() {
return new Client(new Config(host, apiKey));
}
}
```
## 5.4 操作实例
```java
@Service
public class MeiliSearchServiceImpl implements MeiliSearchService {
@Resource
private Client meiliClient;
@Override
public <T> int addDocuments(String indexUid, List<T> documents) {
String jsonArray = JSONUtil.toJsonStr(documents);
TaskInfo taskInfo = meiliClient.index(indexUid).addDocuments(jsonArray);
meiliClient.waitForTask(taskInfo.getTaskUid());
return taskInfo.getTaskUid();
}
@Override
public void updateDocument(String indexUid, Object document) {
TaskInfo task = meiliClient.index(indexUid).updateDocuments(JSONUtil.toJsonStr(document));
meiliClient.waitForTask(task.getTaskUid());
}
@Override
public void deleteDocument(String indexUid, String documentId) {
TaskInfo task = meiliClient.index(indexUid).deleteDocument(documentId);
meiliClient.waitForTask(task.getTaskUid());
}
@Override
public SearchResult search(String indexUid, String query, List<String> highlightFields) {
SearchRequest request = new SearchRequest(query)
.setShowMatchesPosition(true)
.setAttributesToHighlight(highlightFields.toArray(new String[0]));
return (SearchResult) meiliClient.index(indexUid).search(request);
}
}
```
::: tip
这里如果开启高亮搜索会有个bug`Meilisearch ApiException: {Error=APIError: {message='Invalid value type at .attributesToHighlight: expected an array, but found a string: "**[Ljava.lang.String;@10fc5e2f**"', code='invalid_search_attributes_to_highlight', type='invalid_request',`
解决方案
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
```
参考[issue](https://github.com/meilisearch/meilisearch-java/issues/687)
:::

130
docs/Web/Others/Seckill.md Normal file
View File

@@ -0,0 +1,130 @@
---
title: 秒杀系统设计与实战
date: 2026-06-10
---
# 一、简介
&emsp;&emsp;一个典型的秒杀系统通常采用 **限流 + 缓存 + 异步** 的架构:
```plain
客户端
Nginx / 网关(限流)
秒杀接口校验、Redis 预扣库存)
RocketMQ下单消息
订单服务(创建订单、真正扣库存)
MySQL
```
&emsp;&emsp;**限流挡人、缓存决策、异步解耦**。
# 二、实战设计
## 2.1 限流
### 2.1.1 Nginx限流
```conf
# 定义规则
limit_req_zone $binary_remote_addr zone=seckill:10m rate=5r/s;
server {
listen 80 default_server;
listen [::]:80 default_server;
location /seckill-api/ {
# 启用限流
limit_req zone=seckill burst=10 nodelay;
# 返回 429
limit_req_status 429;
proxy_pass http://127.0.0.1:8080/;
}
}
```
&emsp;&emsp;`limit_req_zone`用来制定规则,`limit_req`表示启用规则,如果某个`location`没写,则不会启动规则。
&emsp;&emsp;`zone=seckill:10m`表示开辟一块`10MB`的内存,取名叫`seckill`,专门存`IP`访问记录,`rate=5r/s`表示每个`IP`每秒最多`5`个请求。
&emsp;&emsp;`limit_req zone=seckill`表示启动`seckill`规则,`burst=10`表示突发请求缓冲,`nodelay`表示超出后直接失败。
::: tip
`limit_req_zone`一定要写在`http`模块中,如果是`Linux`部署的,直接写在最外面。
:::
::: warning
Nginx限流只能防止单ip高刷无法挡住大量不同IP的分布式攻击。
:::
### 2.1.2 Semaphore限流
&emsp;&emsp;`Semaphore`是针对应用层限流,保护`JVM`,防止`Tomcat`线程池被打爆。
```java
private final Semaphore semaphore = new Semaphore(100);
if (!semaphore.tryAcquire()) {
return ResponseEntity.status(429).body("系统繁忙");
}
try {
} finally {
semaphore.release();
}
```
&emsp;&emsp;`new Semaphore(100)`表示最多`100`个线程同时进来,超过则直接返回失败,不会进入排队,也不会卡住线程。
::: tip
Semaphore不是全局限流不具备分布式能力。
:::
### 2.1.3 Redis限流
```java
String userKey = "seckill:user:" + userId;
Boolean first = redis.opsForValue().setIfAbsent(userKey, "1", 60, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(first)) {
return ResponseEntity.status(429).body("请勿重复尝试");
}
```
&emsp;&emsp;如果是限制每个用户只能限购一次,则可以将过期时间设置为永久。
## 2.2 缓存
&emsp;&emsp;先在`Redis`里面判断逻辑,然后再执行数据库。
```java
String stockKey = "seckill:stock:1";
Long remain = redis.opsForValue().decrement(stockKey);
if (remain == null || remain < 0) {
redis.opsForValue().increment(stockKey);
return ResponseEntity.status(200).body("已售罄");
}
```
&emsp;&emsp;要先减再判断如果小于0则表示已售罄再回滚数据。
## 2.3 异步
```java
rocketMQTemplate.convertAndSend("seckill-order", Map.of("userId", userId, "goodsId", 1));
return ResponseEntity.status(200).body("秒杀成功");
```
&emsp;&emsp;Redis成功后则通过发消息通知消费者操作数据库。
&emsp;&emsp;为了防止重复创建订单,还需要进行一次幂等校验,即根据订单号的唯一性判断是否有重复的。
# 三、压测
&emsp;&emsp;采用JMeter工具进行压测。
## 3.1 创建测试数据
&emsp;&emsp;新建users.csv输入以下内容
```csv
1
2
3
...
```
&emsp;&emsp;然后在`线程组->添加->配置元件->CSV Data Set Config`设置变量名称为userId。
::: tip
不需要输入表头。
:::
## 3.2 HTTP请求
&emsp;&emsp;设置请求路径为`/seckill/${userId}`设置线程组参数例如线程数1000Ramp-Up时间5秒循环次数为1000创建聚合报告执行线程查看结果。
![JMeter](../images/jmeter.png)

View File

@@ -0,0 +1,243 @@
---
title: 视频压缩
date: 2026-01-29
---
# 一、简介
&emsp;&emsp;视频码率Bit Rate指的是视频文件在单位时间内通常是每秒所包含的数据量。单位通常是 Mbps兆比特每秒或 Kbps千比特每秒
&emsp;&emsp;码率越高:视频中包含的细节信息越多,画面越清晰、色彩越丰富、动态画面(如快速运动、爆炸等)越流畅,不容易出现马赛克或模糊。但代价是文件体积越大,传输所需的网络带宽也越高。码率越低:视频文件更小,传输更快,但在复杂画面中容易产生压缩瑕疵(如马赛克、模糊、色带等)。
&emsp;&emsp;如果不压缩原始视频数据量极其庞大一部2小时的未压缩1080p电影可能占用数TB的存储空间普通硬盘无法承受家庭宽带或移动网络无法实时传输如此巨大的数据量视频播放会不断卡顿。
&emsp;&emsp;如果服务器的带宽为10M适合 720p勉强支持低码率 1080p。
# 二、引入依赖
```xml
<!-- Jave 2 视频处理库 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.5.0</version>
</dependency>
```
# 三、工具类
```java
import lombok.extern.slf4j.Slf4j;
import ws.schild.jave.Encoder;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
import ws.schild.jave.info.VideoSize;
import java.io.File;
/**
* 视频压缩工具类
* 提供将视频压缩到720p的静态方法
*/
@Slf4j
public class VideoCompressor {
// 支持的视频格式
private static final String[] SUPPORTED_FORMATS = {
".mp4", ".avi", ".mov", ".mkv", ".flv",
".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
};
// 压缩参数常量
private static final int TARGET_WIDTH = 1280;
private static final int TARGET_HEIGHT = 720;
private static final int VIDEO_BITRATE = 1500000; // 1.5 Mbps
private static final int AUDIO_BITRATE = 128000; // 128 kbps
private static final String VIDEO_CODEC = "libx264";
private static final String AUDIO_CODEC = "aac";
private static final String OUTPUT_FORMAT = "mp4";
private static final String OUTPUT_SUFFIX = "_720p.mp4";
private static final int MAX_BITRATE = 2500; // 2.5 Mbps
private static final int MIN_BITRATE = 1000; // 1 Mbps
/**
* 压缩视频到720p统一输出为MP4格式
*
* @param inputFile 输入视频文件
* @return 压缩后的视频文件,如果不需要压缩则返回原文件
* @throws Exception 压缩过程中可能出现的异常
*/
public static File compressTo720p(File inputFile) throws Exception {
if (!isSupportedVideoFile(inputFile)) {
log.info("文件格式不支持或文件不存在,直接返回源文件");
return inputFile;
}
// 判断是否需要压缩
if (!needCompression(inputFile)) {
return inputFile;
}
File outputFile = createOutputFile(inputFile);
Encoder encoder = new Encoder();
// 配置编码参数
EncodingAttributes encodingAttributes = createEncodingAttributes();
log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
log.info("视频压缩完成: {}", outputFile.getName());
return outputFile;
}
/**
* 智能判断视频是否需要压缩
* 基于分辨率、码率、数据密度等多维度综合判断
*/
public static boolean needCompression(File videoFile) throws Exception {
MultimediaObject media = new MultimediaObject(videoFile);
// 获取视频基本信息
double fileSizeMB = videoFile.length() / 1048576.0; // 转换为MB
double duration = media.getInfo().getDuration() / 1000.0;
int bitRateKbps = media.getInfo().getVideo().getBitRate() / 1000; // 转换为kbps
// 获取视频分辨率
int width = media.getInfo().getVideo().getSize().getWidth();
int height = media.getInfo().getVideo().getSize().getHeight();
log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率约 {}kbps", width, height, fileSizeMB, duration, bitRateKbps);
// 判断逻辑:基于分辨率和码率的智能判断
boolean needsCompression = false;
// 1. 如果分辨率已经<=720p检查码率是否过高
if (height <= TARGET_HEIGHT && width <= TARGET_WIDTH) {
if (bitRateKbps > MAX_BITRATE) {
log.info("视频分辨率达标但码率({}kbps)过高,需要压缩", bitRateKbps);
needsCompression = true;
} else {
log.info("视频分辨率已达720p或以下且码率合理无需压缩");
}
}
// 2. 如果分辨率>720p但码率很低说明已是优化过的视频无需压缩
else if (height > TARGET_HEIGHT && bitRateKbps < MIN_BITRATE) {
log.info("视频虽为{}p高分辨率但码率({}kbps)已优化,无需压缩", height, bitRateKbps);
}
// 3. 高分辨率+高码率,需要压缩
else if (height > TARGET_HEIGHT && bitRateKbps >= MAX_BITRATE) {
log.info("视频为{}p高分辨率且码率较高({}kbps)需要压缩至720p", height, bitRateKbps);
needsCompression = true;
}
// 4. 其他情况(如异常数据)默认不压缩
else {
log.info("视频参数异常,保守处理:不进行压缩");
}
return needsCompression;
}
/**
* 检查文件是否为支持的视频格式
*/
private static boolean isSupportedVideoFile(File file) {
if (file == null || !file.exists()) {
return false;
}
String filename = file.getName().toLowerCase();
for (String format : SUPPORTED_FORMATS) {
if (filename.endsWith(format)) {
return true;
}
}
return false;
}
/**
* 创建输出文件对象
*/
private static File createOutputFile(File inputFile) {
String baseName = getFileBaseName(inputFile.getName());
String outputFilename = baseName + OUTPUT_SUFFIX;
return new File(inputFile.getParent(), outputFilename);
}
/**
* 获取文件名(不含扩展名)
*/
private static String getFileBaseName(String filename) {
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
}
/**
* 创建编码配置参数
*/
private static EncodingAttributes createEncodingAttributes() {
// 音频配置
AudioAttributes audio = new AudioAttributes();
audio.setCodec(AUDIO_CODEC);
audio.setBitRate(AUDIO_BITRATE);
// 视频配置
VideoAttributes video = new VideoAttributes();
video.setCodec(VIDEO_CODEC);
video.setBitRate(VIDEO_BITRATE);
video.setSize(new VideoSize(TARGET_WIDTH, TARGET_HEIGHT));
// 编码设置
EncodingAttributes attributes = new EncodingAttributes();
attributes.setOutputFormat(OUTPUT_FORMAT);
attributes.setAudioAttributes(audio);
attributes.setVideoAttributes(video);
return attributes;
}
}
```
&emsp;&emsp;通过分辨率和码率的双重判断,确定是否需要压缩。
# 四、使用
```java
public void uploadFile(MultipartFile file, String filePath) {
try {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
File tempFile = convertMultipartFileToFile(file);
log.info("临时文件地址:{}", tempFile.toPath());
log.info("开始压缩文件:{}", tempFile.getName());
File compressFile = VideoCompressor.compressTo720p(tempFile);
log.info("完成压缩文件:{}", tempFile.getName());
log.info("压缩文件地址:{}", tempFile.toPath());
body.add("file", new FileSystemResource(compressFile));
aListClient.put()
.uri("api/fs/form")
.header(HttpHeaders.AUTHORIZATION, token)
.header("File-Path", filePath)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.body(Map.class);
Files.deleteIfExists(compressFile.toPath());
Files.deleteIfExists(tempFile.toPath());
} catch (Exception e) {
log.error("文件上传失败{}", e.getMessage());
throw new RuntimeException("文件上传失败", e);
}
}
private File convertMultipartFileToFile(MultipartFile file) throws IOException {
String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
: ".tmp";
Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
file.transferTo(tempFile);
return tempFile.toFile();
}
```

View File

@@ -0,0 +1,222 @@
---
title: Spring AOP简介
date: 2025-12-25
---
# 一、基本概念
&emsp;&emsp;AOPAspect-Oriented Programming面向切面编程是一种编程范式核心思想是**将与业务核心逻辑无关但又被多个模块共享的功能(如日志、事务、权限校验、性能监控)抽取出来,形成独立的 “切面”**,在不修改原有业务代码的前提下,通过 “织入” 的方式动态添加到目标方法的执行流程中。
&emsp;&emsp;Spring AOP 是 Spring 框架对 AOP 思想的实现,基于**动态代理JDK 动态代理 + CGLIB 动态代理)**,专注于解决企业级开发中的横切关注点问题。
| 术语 | 说明 |
|------|------|
| 切面Aspect | 封装横切逻辑的类(如日志切面、事务切面),包含通知和切点 |
| 连接点Joinpoint | 程序执行过程中的某个特定点如方法调用、异常抛出Spring AOP 中仅支持方法级连接点 |
| 切点Pointcut | 匹配连接点的规则,用于指定切面要织入到哪些方法上 |
| 通知Advice | 切面的具体逻辑(如日志打印、权限校验),按执行时机分为 5 种 |
| 目标对象Target | 被切面织入的目标类(业务类) |
| 代理对象Proxy | Spring AOP 为目标对象创建的代理类,横切逻辑通过代理执行 |
| 织入Weaving | 将切面逻辑添加到目标对象的过程Spring AOP 是运行时织入(通过动态代理) |
| 通知类型 | 执行时机 |
|----------|----------|
| 前置通知Before | 目标方法执行前执行 |
| 后置通知After | 目标方法执行后执行(无论是否抛出异常) |
| 返回通知AfterReturning | 目标方法正常返回后执行 |
| 异常通知AfterThrowing | 目标方法抛出异常后执行 |
| 环绕通知Around | 包裹目标方法执行,可自定义目标方法执行前后的逻辑(功能最强) |
&emsp;&emsp;[AOP API](https://docs.springjava.cn/spring-framework/reference/core/aop-api.html)
# 二、原理
&emsp;&emsp;Spring AOP 基于动态代理实现,分为两种方式:
1. JDK 动态代理
通过 java.lang.reflect.Proxy 生成代理类,代理类实现目标接口,调用 InvocationHandler 处理横切逻辑;基于接口,无侵入。
2. CGLIB 动态代理
通过继承目标类生成子类作为代理类,重写目标方法,织入横切逻辑;基于继承,目标类不能是 final方法不能是 final。
&emsp;&emsp;Spring AOP 自动选择规则:如果目标类实现了接口,使用 JDK 动态代理;否则使用 CGLIB。
# 三、实战
&emsp;&emsp;例如存在以下业务模块:
```java
@Service
public class OrderService {
// 模拟创建订单
public String createOrder(String orderNo) {
System.out.println("创建订单:" + orderNo);
return "订单创建成功:" + orderNo;
}
// 模拟删除订单(故意抛出异常)
public void deleteOrder(String orderNo) {
System.out.println("删除订单:" + orderNo);
throw new RuntimeException("删除订单失败:订单不存在");
}
}
```
&emsp;&emsp;AOP实现
```java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
// 1. 标记为切面类
@Aspect
// 2. 交给Spring容器管理
@Component
public class LogAspect {
// 3. 定义切点匹配OrderService类的所有方法
@Pointcut("execution(* com.example.aopdemo.service.OrderService.*(..))")
public void orderServicePointcut() {}
// 4. 前置通知:目标方法执行前执行
@Before("orderServicePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名和参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[前置通知] 方法:" + methodName + ",参数:" + Arrays.toString(args));
}
// 5. 后置通知:目标方法执行后执行(无论是否异常)
@After("orderServicePointcut()")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置通知] 方法:" + methodName + " 执行完毕");
}
// 6. 返回通知:目标方法正常返回后执行
@AfterReturning(value = "orderServicePointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[返回通知] 方法:" + methodName + ",返回值:" + result);
}
// 7. 异常通知:目标方法抛出异常后执行
@AfterThrowing(value = "orderServicePointcut()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[异常通知] 方法:" + methodName + ",异常:" + ex.getMessage());
}
// 8. 环绕通知:包裹目标方法执行(功能最强)
@Around("orderServicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String methodName = proceedingJoinPoint.getSignature().getName();
Object[] args = proceedingJoinPoint.getArgs();
// 前置逻辑
System.out.println("[环绕通知-前置] 方法:" + methodName + ",参数:" + Arrays.toString(args));
Object result = null;
try {
// 执行目标方法
result = proceedingJoinPoint.proceed();
// 返回后逻辑
System.out.println("[环绕通知-返回] 方法:" + methodName + ",返回值:" + result);
} catch (Throwable e) {
// 异常逻辑
System.out.println("[环绕通知-异常] 方法:" + methodName + ",异常:" + e.getMessage());
throw e; // 必须抛出异常,否则上层无法捕获
} finally {
// 后置逻辑
System.out.println("[环绕通知-后置] 方法:" + methodName + " 执行完毕");
}
return result;
}
}
```
&emsp;&emsp;测试:
```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
// 测试正常方法createOrder
@Test
public void testCreateOrder() {
orderService.createOrder("ORDER_20251225");
}
// 测试异常方法deleteOrder
@Test
public void testDeleteOrder() {
try {
orderService.deleteOrder("ORDER_9999");
} catch (Exception e) {
// 捕获异常,避免测试中断
}
}
}
```
&emsp;&emsp;测试结果:
```plaintext
[环绕通知-前置] 方法createOrder参数[ORDER_20251225]
[前置通知] 方法createOrder参数[ORDER_20251225]
创建订单ORDER_20251225
[环绕通知-返回] 方法createOrder返回值订单创建成功ORDER_20251225
[返回通知] 方法createOrder返回值订单创建成功ORDER_20251225
[后置通知] 方法createOrder 执行完毕
[环绕通知-后置] 方法createOrder 执行完毕
[环绕通知-前置] 方法deleteOrder参数[ORDER_9999]
[前置通知] 方法deleteOrder参数[ORDER_9999]
删除订单ORDER_9999
[环绕通知-异常] 方法deleteOrder异常删除订单失败订单不存在
[异常通知] 方法deleteOrder异常删除订单失败订单不存在
[后置通知] 方法deleteOrder 执行完毕
[环绕通知-后置] 方法deleteOrder 执行完毕
```
# 四、切点表达式
| 切点表达式 | 说明 |
|------------|------|
| `execution(* com.example.service.*.*(..))` | 匹配 com.example.service 包下所有类的所有方法 |
| `execution(public String com.example.service.OrderService.create*(String))` | 匹配 OrderService 类中公共、返回值为 String、方法名以 create 开头、参数为 String 的方法 |
| `execution(* com.example..*Service.*(..))` | 匹配 com.example 包及其子包下所有以 Service 结尾的类的所有方法 |
| `@annotation(com.example.annotation.Log)` | 匹配标注了 @Log 自定义注解的方法 |
&emsp;&emsp;组合切点:
```java
// 匹配OrderService的方法 且 方法名包含"Order"
@Pointcut("execution(* com.example.service.OrderService.*(..)) && execution(* *Order*(..))")
public void combinePointcut() {}
```
# 五、总结
&emsp;&emsp;通俗的解释AOP本质是找目标通过切点规则精准找到要 “加戏” 的方法 / 类)→ 造代理Spring 自动给这些目标生成一个 “中间商”(代理对象))→ 加戏(在目标方法真正执行前 / 后 / 出问题时,让这个 “中间商” 帮你干额外的活)。
::: tip
基于 Spring AOP 的知名框架/组件汇总
| 框架/组件 | 核心作用 | Spring AOP 应用说明 |
|-----------|----------|---------------------|
| AspectJ | 提供切点表达式语法、注解解析(@Aspect/@Pointcut 等) | Spring AOP 仅复用其语法规则,底层仍为自身动态代理(未使用 AspectJ 编译期织入) |
| Spring Core | 容器管理、动态代理创建JDK/CGLIB | 负责切面/目标类的生命周期管理,生成代理对象并完成织入逻辑 |
| Spring Transaction | 声明式事务管理 | 切点:匹配 @Transactional 注解方法;<br>通知:方法执行前开启事务、执行后提交、异常时回滚 |
| Spring Cache | 声明式缓存管理 | 切点:匹配 @Cacheable/@CacheEvict 等注解方法;<br>通知:方法执行前查缓存、执行后更新缓存 |
| spring-boot-starter-aop | AOP 基础依赖封装 | 自动引入 AspectJ + Spring AOP 核心,开箱即用 |
| Spring Security | 安全权限管理 | 切点:匹配 @PreAuthorize 等权限注解方法;<br>通知:方法执行前校验用户权限,无权限抛异常 |
| MyBatis-Plus | 增强 MyBatis CRUD 操作 | 切点:匹配 Mapper 接口方法/标注注解的实体类;<br>通知:织入分页逻辑、注解(@TableName)解析等 |
| Shiro | 权限管理框架 | 切点:匹配 @RequiresPermissions 注解方法;<br>通知:方法执行前校验用户权限 |
| Seata | 分布式事务框架 | 切点:匹配 @GlobalTransactional 注解方法;<br>通知:织入全局事务的开启、提交、回滚逻辑 |
| Sentinel | 流量控制/熔断降级框架 | 切点:匹配 @SentinelResource 注解方法;<br>通知:方法执行前校验流量阈值、熔断规则 |
| Dynamic-TP | 动态线程池框架 | 切点:匹配线程池执行方法;<br>通知:织入线程池耗时、队列长度等监控逻辑 |
| p6spy | 数据库 SQL 监控 | 切点:拦截 JDBC 操作方法;<br>通知:织入 SQL 打印、执行耗时统计逻辑 |
| dynamic-datasource-spring-boot-starter | 动态数据源切换 | 切点:匹配 @DS 注解方法;<br>通知:方法执行前切换数据源、执行后恢复默认数据源 |
| xxl-job | 分布式任务调度 | 切点:匹配任务执行方法;<br>通知:织入任务执行日志、监控指标采集逻辑 |
| Spring Cloud Sleuth | 微服务链路追踪 | 切点:拦截 HTTP/RPC 调用方法;<br>通知:织入 traceId/spanId 传递、链路信息采集逻辑 |
:::

View File

@@ -4,7 +4,8 @@
---
# 一、IOC
&emsp;&emsp;Spring框架的核心是IOC控制反转容器它负责管理应用程序中的对象称为Bean的创建、配置和组装。
&emsp;&emsp;Spring框架的核心是IOC控制反转容器它负责管理应用程序中的对象称为Bean的创建、配置和组装。
&emsp;&emsp;[IoC 容器](https://docs.springjava.cn/spring-framework/reference/core/beans.html)
# 二、Bean
## 2.1 定义
@@ -38,6 +39,12 @@ public class AppConfig {
<bean id="userService" class="com.example.UserService"/>
```
::: tip
@Service@Controller这些注解是@Component的衍生注解
@Component注解可以通过@Autowired自动注入,一般用于自己编写的业务模块,需要配合@ComponentScan启用组件扫描(实际开发中@SpringBootApplication注解已经包含了@ComponentScan注解)。
@Bean注解一般用于注册第三方库的类
:::
## 2.4 作用域
&emsp;&emsp;控制 Bean 在容器中的实例数量和生命周期。Spring 提供多种作用域,默认是 singleton。通过 @Scope 注解指定,如 @Scope("prototype")。
- singleton单例容器中只有一个实例所有请求共享该实例默认值
@@ -46,25 +53,32 @@ public class AppConfig {
- sessionWeb每个会话创建一个实例在会话有效期内有效。
## 2.5 生命周期
1. 实例化Instantiation:容器通过构造器创建 Bean 实例(分配内存)。
2. 属性注入Population容器将依赖的 Bean 注入到当前 Bean 的字段或方法(如 @Autowired 标注的依赖)。
3. 初始化前Post-processing before initialization
执行 BeanPostProcessor 的 postProcessBeforeInitialization 方法AOP 代理生成在此阶段)。
4. 初始化Initialization
调用 @PostConstruct 注解的方法JSR-250 标准,推荐)。
若实现 InitializingBean 接口,调用 afterPropertiesSet() 方法。
执行自定义初始化方法(如 @Bean(initMethod = "init") 中指定的 init 方法)。
5. 初始化后Post-processing after initialization
执行 BeanPostProcessor 的 postProcessAfterInitialization 方法。
6. 使用In UseBean 被应用程序调用。
7. 销毁Destruction
调用 @PreDestroy 注解的方法JSR-250 标准,推荐)。
若实现 DisposableBean 接口,调用 destroy() 方法。
执行自定义销毁方法(如 @Bean(destroyMethod = "destroy") 中指定的 destroy 方法)。
1. 实例化Instantiation
容器通过构造器创建 Bean 实例(分配内存)。
2. 属性注入Population
容器将依赖的 Bean 注入到当前 Bean 的字段或方法(如 @Autowired 标注的依赖)。
3. 初始化前Post-processing before initialization
执行 BeanPostProcessor 的 postProcessBeforeInitialization 方法AOP 代理生成在此阶段)。
4. 初始化Initialization
调用 @PostConstruct 注解的方法JSR-250 标准,推荐)。
若实现 InitializingBean 接口,调用 afterPropertiesSet() 方法。
执行自定义初始化方法(如 @Bean(initMethod = "init") 中指定的 init 方法)。
5. 初始化后Post-processing after initialization
执行 BeanPostProcessor 的 postProcessAfterInitialization 方法。
6. 使用In Use
Bean 被应用程序调用。
7. 销毁Destruction
调用 @PreDestroy 注解的方法JSR-250 标准,推荐)。
若实现 DisposableBean 接口,调用 destroy() 方法。
执行自定义销毁方法(如 @Bean(destroyMethod = "destroy") 中指定的 destroy 方法)。
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/b1054386c885ffefb0f08074ad6b6268.png 'Bean生命周期.png')
# 三、BeanDefinition
## 3.1 定义
&emsp;&emsp;BeanDefinition是Spring框架中描述Bean的元数据对象它包含了创建Bean实例所需的所有信息。
@@ -172,7 +186,7 @@ public interface BeanFactory {
}
```
## 4.4 实现示例
## 4.4 使用示例
```java
// 创建BeanFactory实例
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
@@ -193,7 +207,7 @@ UserService userService = beanFactory.getBean("userService", UserService.class);
- 延迟加载默认在第一次请求时才创建Bean实例
- 基础功能提供最基本的IOC功能
# 五、ApplicationContext
# 五、ApplicationContext ★★★
## 5.1 定义
&emsp;&emsp;ApplicationContext是BeanFactory的子接口在BeanFactory的基础上提供了更多企业级功能是Spring容器的完整实现。
@@ -544,5 +558,5 @@ protected void finishRefresh() {
- 标记整个容器启动流程结束,容器进入就绪状态
### 5.4.13 总结
&emsp;&emsp;refresh()方法按固定顺序执行12个步骤核心可归纳为准备容器环境 → 解析并注册Bean定义→ 初始化事件、消息等基础设施 → 实例化所有非懒加载的单例Bean→ 完成启动并发布事件。整个过程通过模板方法模式定义,确保了扩展性和一致性。
&emsp;&emsp;核心在于两个关键扩展机制BeanFactoryPostProcessor在Bean定义阶段介入负责修改和注册Bean的定义信息BeanPostProcessor在Bean实例化阶段介入负责对创建好的Bean实例进行增强和包装。
&emsp;&emsp;refresh()方法按固定顺序执行12个步骤核心可归纳为**准备容器环境 → 解析并注册Bean定义→ 初始化事件、消息等基础设施 → 实例化所有非懒加载的单例Bean→ 完成启动并发布事件**。整个过程通过模板方法模式定义,确保了扩展性和一致性。
&emsp;&emsp;核心在于两个关键扩展机制:**BeanFactoryPostProcessor在Bean定义阶段介入负责修改和注册Bean的定义信息BeanPostProcessor在Bean实例化阶段介入负责对创建好的Bean实例进行增强和包装。**

View File

@@ -4,37 +4,38 @@
---
# 一、基础概念
&emsp;&emsp;Spring MVCSpring Model-View-Controller是 Spring Framework 中的一部分,它是一个基于 **<font style="color:#DF2A3F;">请求驱动</font>** 的 Web 框架,主要用于构建 Web 应用程序,并且遵循 MVC模型-视图-控制器) 设计模式。它将 Web 应用的业务逻辑、用户界面和请求处理分离,使得代码更加模块化和可维护。
&emsp;&emsp;Spring MVCSpring Model-View-Controller是 Spring Framework 中的一部分,它是一个基于 **请求驱动** 的 Web 框架,主要用于构建 Web 应用程序,并且遵循 MVC模型-视图-控制器) 设计模式。它将 Web 应用的业务逻辑、用户界面和请求处理分离,使得代码更加模块化和可维护。
&emsp;&emsp;[Spring Web MVC](https://docs.springjava.cn/spring-framework/reference/web/webmvc.html)
# 二、具体流程
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/04fb16a3e32f247bbf0bb7e176de8444.png '具体流程.png')
1. **客户端发起HTTP请求**
1. 客户端发起HTTP请求
&emsp;&emsp;客户端浏览器、API 客户端等)向服务器发送 HTTP 请求。请求包含了目标 URL、HTTP 方法GET、POST 等)以及可能的请求参数。
前端通过发送 HTTP 请求(通常是 AJAX 请求)到后端的 API 端点,并期望获得响应(通常是 JSON 数据)。
2. **请求到达 DispatcherServlet**
2. 请求到达 DispatcherServlet
&emsp;&emsp;所有 HTTP 请求都会先到达 Spring MVC 的核心组件:**<font style="color:#DF2A3F;">DispatcherServlet</font>**。它是前端控制器负责将请求分发给正确的处理器。DispatcherServlet 是所有请求的入口Spring Boot 会自动配置它。
&emsp;&emsp;所有 HTTP 请求都会先到达 Spring MVC 的核心组件:**DispatcherServlet**。它是前端控制器负责将请求分发给正确的处理器。DispatcherServlet 是所有请求的入口Spring Boot 会自动配置它。
&emsp;&emsp;DispatcherServlet 接受所有的 HTTP 请求,根据请求 URL 和 HTTP 方法,定位匹配的控制器方法,将请求交给相应的 Handler Mapping 来匹配控制器。在请求处理前后,执行 拦截器(可选)。最终通过 View Resolver 返回一个合适的视图(对于 RESTful API通常返回 JSON 格式的数据)。
3. **查找处理器Handler Mapping**
3. 查找处理器Handler Mapping
&emsp;&emsp;DispatcherServlet 根据请求的 URL 和 HTTP 方法,查找与之匹配的处理器方法(通常是一个控制器中的方法)。这是由 HandlerMapping 完成的。Spring MVC 提供了多种方式来映射 URL 到控制器方法,例如 @RequestMapping@GetMapping 等注解。
&emsp;&emsp;@RestController:该注解标记的类会返回 JSON 数据。
&emsp;&emsp;@RequestMapping:用于映射 URL 路径和 HTTP 方法。
&emsp;&emsp;@GetMapping@PostMapping 等:简化版本的 @RequestMapping
4. **执行拦截器Interceptor**
4. 执行拦截器Interceptor
&emsp;&emsp;在请求到达控制器之前DispatcherServlet 会依次执行配置的拦截器。拦截器主要用于日志记录、安全检查、请求参数的预处理等。
&emsp;&emsp;preHandle请求进入控制器方法之前。
&emsp;&emsp;postHandle请求完成后视图渲染之前。
&emsp;&emsp;afterCompletion视图渲染完成后。
&emsp;&emsp;**<font style="color:#DF2A3F;">如果拦截器的 preHandle 方法返回 false请求会被拦截后续的处理不会继续进行。</font>**
&emsp;&emsp;**如果拦截器的 preHandle 方法返回 false请求会被拦截后续的处理不会继续进行。**
5. **控制器方法处理请求Handler Execution**
5. 控制器方法处理请求Handler Execution
&emsp;&emsp;DispatcherServlet 调用合适的控制器方法来处理请求。控制器方法负责处理请求的业务逻辑,并返回一个 Java 对象,这个对象会被自动转换成 JSON 格式返回给客户端。
&emsp;&emsp;Spring 通过 @ResponseBody 注解(或 @RestController)实现将 Java 对象自动转换为 JSON 格式。默认情况下Spring 使用 Jackson 来将 Java 对象序列化成 JSON 数据。
@@ -43,7 +44,7 @@
&emsp;&emsp;@RequestParam:用于获取单个请求参数。
&emsp;&emsp;@PathVariable:用于从 URL 中提取变量(如 /users/{id} 中的 {id})。
6. **响应返回客户端**
6. 响应返回客户端
&emsp;&emsp;DispatcherServlet 将处理完的结果JSON 数据)返回给客户端。响应数据通常会包含:
&emsp;&emsp;**状态码**:如 200 OK成功、201 Created资源创建成功、404 Not Found资源未找到、500 Internal Server Error服务器错误等。

View File

@@ -0,0 +1,72 @@
---
title: SpingBoot注解
date: 2025-12-30
---
# 一、注解
## 1.1 定义
&emsp;&emsp;注解Annotation是 JDK5.0 引入的特性,可以理解为:给代码添加的 “元数据”(描述数据的数据),就像给代码贴标签,**本身不直接影响代码执行,但可以被编译器、框架(如 SpringBoot读取并做相应处理**。
## 1.2 分类
| 类型 | 核心作用 | 典型示例 |
|----------------|--------------------|------------------|
| 源码注解 | 仅在源码编译阶段生效,编译后注解消失 | `@Override` |
| 编译时注解 | 编译期生效注解信息保留到class文件但JVM运行时不加载 | Lombok的`@Data` |
| 运行时注解 | 整个生命周期都存在(源码→编译→运行),可通过反射动态获取注解信息 | SpringBoot的`@RestController``@Service``@Transactional` |
## 1.3 示例
```java
import java.lang.annotation.*;
// 1. 注解的元注解(描述注解的注解)
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射获取
@Documented // 生成Javadoc时包含该注解
public @interface MyAnnotation {
// 注解的属性(类似方法,可设置默认值)
String value() default "默认描述";
int num() default 0;
}
// 2. 使用自定义注解
public class AnnotationTest {
@MyAnnotation(value = "测试方法", num = 10)
public void test() {
System.out.println("执行测试方法");
}
// 3. 通过反射读取注解
public static void main(String[] args) throws NoSuchMethodException {
// 获取方法对象
java.lang.reflect.Method method = AnnotationTest.class.getMethod("test");
// 判断方法是否有该注解
if (method.isAnnotationPresent(MyAnnotation.class)) {
// 获取注解实例
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
// 读取注解属性
System.out.println("注解value" + annotation.value()); // 输出:测试方法
System.out.println("注解num" + annotation.num()); // 输出10
}
}
}
```
&emsp;&emsp;@Target指定注解能作用的位置如ElementType.METHOD= 方法、ElementType.TYPE= 类 / 接口、ElementType.FIELD= 字段)。
&emsp;&emsp;@Retention指定注解的保留阶段RUNTIME是 SpringBoot 注解最常用的)
&emsp;&emsp;首先定义注解的@Target和@Retention信息然后设置注解的属性类似于方法的参数比如这里的value和num需要在使用的时候通过命名参数的形式传递过来。如果是RUNTIME类型的注解可以通过反射来获取方法和注解的参数实现自定义逻辑功能。
# 二、SpringBoot核心注解
## 2.1 启动类注解
&emsp;&emsp;@SpringBootApplication为SpringBoot启动类注解,由以下三个注解组成:
1. @Configuration:标记类为配置类(替代 XML 配置)
2. @EnableAutoConfiguration开启自动配置SpringBoot 核心,自动配置 Tomcat、数据库连接等
3. @ComponentScan:扫描当前包及子包下的 @Component@Service 等注解的类,纳入 Spring 容器管理
## 2.2 组件注册注解
| 注解 | 作用 | 使用场景 |
|----------------|--------------------------|------------------------------------------------------------------|
| @Component | 通用组件注解 | 通用工具类、非业务层类 |
| @Controller | 控制器注解 | MVC 的控制层(返回页面) |
| @RestController| REST 控制器 | API 接口层(返回 JSON/XML= @Controller + @ResponseBody |
| @Service | 服务层注解 | 业务逻辑层 |
| @Repository | 数据访问层注解 | DAO 层 / 持久层(如 MyBatis 的 Mapper 接口) |

View File

@@ -0,0 +1,120 @@
---
title: SpringBoot Quarzt简介和使用
date: 2026-06-08
---
# 一、基本概念
&emsp;&emsp;Quartz 是 Java 生态最主流的企业级作业调度框架支持简单间隔触发、Cron 表达式、持久化到数据库、集群部署。
- Job任务 — 实现 execute()方法,写你要执行的业务逻辑
- Trigger触发器 — 定义"什么时候执行",常用 SimpleTrigger固定间隔和 CronTriggerCron 表达式)
- Scheduler调度器 — 把 Job + Trigger 绑定,负责启动和停止调度
# 二、原理
&emsp;&emsp;Job相当于员工只知道干活Trigger 相当于排班表决定什么时候干活Scheduler 相当于老板,统一管理 Job 和 Trigger。
&emsp;&emsp;Quartz内部有一个核心线程 QuartzSchedulerThread会不停的检查哪个员工需要在啥时候干活但并不是按照固定间隔轮询的方式而是计算出最近一次 Trigger 的触发时间,通过 wait()精确等待,当等待结束后,调度线程会唤醒线程池中的工作线程,执行到点的 Job。如果当前没有任何 Trigger了默认最多等待 30 秒,然后再重新扫描​。如果此时有新增/修改/删除任务了,调度线程会被 notifyAll()立即唤醒,重新计算最近的触发时间。
# 三、安装
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
```
&emsp;&emsp;持久化:
```yml
spring:
quartz:
job-store-type: jdbc
```
&emsp;&emsp;启用后会在数据库自动建表。
# 四、使用
## 4.1 定义Job
```java
@Component
@DisallowConcurrentExecution // 防止同一 Job 并发执行
public class QuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) {
// 获取任务内容
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// TODO 根据任务内容执行任务
}
}
```
## 4.2 定义Trigger
```java
@Component
public class QuartzUtils {
public static JobDetail buildJobDetail(Task task, JobKey jobKey) {
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("id", task.getId());
jobDataMap.put("type", task.getType());
jobDataMap.put("content", task.getContent());
return JobBuilder.newJob(QuartzJob.class)
.withIdentity(jobKey) // 使用 id 作为唯一标识
.usingJobData(jobDataMap)
.build();
}
public static Trigger buildTrigger(Task task, JobKey jobKey) {
Trigger trigger;
// 判断任务类型Cron 任务或者一次性任务
if (task.getCronExpression() != null && !task.getCronExpression().isEmpty()) {
// 定时任务:使用 Cron 表达式
trigger = TriggerBuilder.newTrigger()
.withIdentity(jobKey.getName() + "Trigger")
.withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression()))
.forJob(jobKey)
.build();
} else if (task.getExecuteTime() != null) {
// 一次性任务:使用 executeTime
trigger = TriggerBuilder.newTrigger()
.withIdentity(jobKey.getName() + "Trigger")
.startAt(task.getExecuteTime())
.forJob(jobKey)
.build();
} else {
// 如果没有 Cron 表达式和 executeTime任务不能调度抛出异常
throw new CustomException("任务没有有效的 Cron 表达式或执行时间");
}
return trigger;
}
}
```
::: tip
这里的Task可以根据实际情况定义:
```java
@Data
public class Task {
private Long id;
private String type;
private String content;
private Date executeTime;
private String cronExpression;
}
```
:::
## 4.3 执行Scheduler
```java
@Resource
private Scheduler scheduler;
// 新增/更新
JobKey jobKey = new JobKey(task.getId().toString());
scheduler.scheduleJob(QuartzUtils.buildJobDetail(task, jobKey), QuartzUtils.buildTrigger(task, jobKey));
// 更新
scheduler.rescheduleJob(QuartzUtils.buildJobDetail(task, jobKey), QuartzUtils.buildTrigger(task, jobKey));
// 删除
scheduler.deleteJob(jobKey);
```

View File

@@ -0,0 +1,170 @@
---
title: SpringBoot Redis简介和使用
date: 2026-03-22
---
# 一、简介
&emsp;&emsp;Redis是一个高性能的开源内存数据库用作缓存、数据库和消息中间件。它以极快的读写速度基于内存存储支持字符串、列表、哈希等多种数据结构。Redis提供数据持久化、主从复制、哨兵模式等高可用特性广泛应用于缓存、会话存储、排行榜、消息队列等场景。
# 二、安装
## 2.1 apt安装
1. 通过apt包管理器安装
```bash
sudo apt update
sudo apt install redis-server
sudo systemctl enable redis-server
```
2. 配置远程和密码访问(可选)
&emsp;&emsp;打开`/etc/redis/redis.conf`文件,更改配置:
```conf
bind 0.0.0.0 ::1
protected-mode yes
requirepass password
```
&emsp;&emsp;`systemctl restart redis-server`重启服务。
## 2.2 docker安装
```yml
services:
redis:
image: redis:8.6.4
container_name: redis
restart: always
ports:
- "6379:6379"
volumes:
- ./redis.conf:/usr/local/etc/redis/redis.conf
- ./data:/data
command: redis-server /usr/local/etc/redis/redis.conf
```
&emsp;&emsp;redis.conf:
```conf
bind 0.0.0.0
protected-mode yes
port 6379
requirepass password
```
## 2.3 客户端
[Tiny RDM](https://redis.tinycraft.cc/zh/)
# 三、SpringBoot使用
## 3.1 引入依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
## 3.2 配置文件
```yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: 123456
```
::: tip
SpringBoot2中没有data层级
:::
## 3.3 简单使用
```java
@Component
public class RedisUtils {
@Resource
private StringRedisTemplate redis;
public void set(String key, String value, long seconds) {
redis.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
}
public String get(String key) {
return redis.opsForValue().get(key);
}
public void delete(String key) {
redis.delete(key);
}
public Boolean hasKey(String key) {
return redis.hasKey(key);
}
// 匹配查询
public Set<String> scanKeys(String pattern) {
return redis.execute((RedisCallback<Set<String>>) connection -> {
Set<String> keys = new HashSet<>();
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();
try (Cursor<byte[]> cursor = connection.keyCommands().scan(options)) {
while (cursor.hasNext()) {
keys.add(new String(cursor.next(), StandardCharsets.UTF_8));
}
} catch (Exception e) {
throw new RuntimeException("Redis SCAN 执行失败pattern=" + pattern, e);
}
return keys;
});
}
}
```
::: tip
生产环境中使用`scan`命令批量查询`key`值,`keys`命令会阻塞`Redis`线程。
:::
# 四、实战
## 4.1 设备在线功能
&emsp;&emsp;实现一个统计设备是否实时在线的功能。
```java
@RestController
@RequestMapping("/device")
public class DeviceController {
@Resource
private RedisUtils redisUtils;
private static final String HEARTBEAT_KEY = "device:heartbeat:";
private static final long EXPIRE_SECONDS = 40;
/**
* 设备心跳接口
* 设备每20-30秒调用一次
* 心跳key 40秒后过期过期即认为设备离线
*/
@PostMapping("/heartbeat")
public Boolean heartbeat(@RequestParam String deviceId) {
redisUtils.set(HEARTBEAT_KEY + deviceId, String.valueOf(System.currentTimeMillis()), EXPIRE_SECONDS);
return Boolean.TRUE;
}
/**
* 获取当前在线设备列表
*/
@GetMapping("/online")
public Set<String> getOnlineDevices() {
Set<String> keys = redisUtils.scanKeys(HEARTBEAT_KEY + "*");
Set<String> deviceIds = new HashSet<>();
for (String key : keys) {
String deviceId = key.substring(HEARTBEAT_KEY.length());
deviceIds.add(deviceId);
}
return deviceIds;
}
}
```
&emsp;&emsp;客户端每30秒调用一次`/device/heartbeat`接口。

View File

@@ -0,0 +1,134 @@
---
title: SpringBoot RestClient简介
date: 2026-01-06
---
# 一、简介
&emsp;&emsp;RestClient 是 Spring Framework 6.1(及对应的 Spring Boot 3.2+)推出的新一代同步 HTTP 客户端,设计目标是替代传统的 RestTemplateSpring 已标记 RestTemplate 为维护模式,不再新增功能),同时结合了 WebClient 的流畅 API 设计,又保持了 RestTemplate 的同步、简单易用的特点。
# 二、使用
## 2.1 创建实例
```java
@Configuration
public class RestClientConfig {
final String baseUrl = "https://demo.com";
@Bean
public RestClient restClient() {
return RestClient.builder()
// 基础RURL
.baseUrl(baseUrl)
// 默认请求头 请求体为JSON格式
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
// 默认请求头 响应体为JSON格式
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
```
## 2.2 HTTP请求
```java
@Service
public class RestClientService {
@Resource
private RestClient restClient;
// 1. GET 请求:获取单个资源
public Post getPostById(Long id) {
return restClient.get() // 指定 GET 方法
.uri("/posts/{id}", id) // 请求路径(支持路径参数)
.retrieve() // 执行请求并获取响应
.body(Post.class); // 解析响应体为Post实体
}
// 2. GET 请求:获取列表并解析为 Map
public Map<String, Object>[] getAllPosts() {
return restClient.get()
.uri("/posts")
.retrieve()
.body(Map[].class); // 解析为 Map 数组(适合未知结构的 JSON
}
// 3. POST 请求:提交数据并获取响应
public Boolean createPost(Post post) {
return restClient.post() // 指定 POST 方法
.uri("/posts")
.body(post) // 设置请求体
.retrieve()
.body(Boolean.class);
}
// 4. PUT 请求:更新资源
public Boolean updatePost(Long id, Post post) {
return restClient.put() // 指定 PUT 方法
.uri("/posts/{id}", id)
.body(post)
.retrieve()
.body(Boolean.class);
}
// 5. DELETE 请求:删除资源
public void deletePost(Long id) {
restClient.delete() // 指定 DELETE 方法
.uri("/posts/{id}", id)
.retrieve(); // DELETE 请求通常无响应体
}
}
```
::: tip
在.retrieve()之前的是请求阶段,在此之后是响应阶段。
因此在此之前的.body()是请求体,在此之后的.body()是响应体。
:::
## 2.3 进阶用法
### 2.3.1 设置超时时间
```java
@Bean
public RestClient restClient() {
// 创建请求工厂,设置超时
ClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() {{
setConnectTimeout(3000); // 连接超时 3 秒
setReadTimeout(5000); // 读取超时 5 秒
}};
return RestClient.builder()
.baseUrl(baseUrl)
.requestFactory(factory) // 设置请求工厂
.build();
}
```
### 2.3.2 添加请求拦截器
```java
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl(baseUrl)
// 添加拦截器
.requestInterceptor(request -> {
request.getHeaders().add("Authorization", "Bearer your-token-here");
})
.build();
}
```
### 2.3.3 自定义响应处理
```java
public String getPostWithErrorHandling(Long id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
// 自定义状态码处理
.onStatus(status -> status.is4xxClientError(), (request, response) -> {
throw new RuntimeException("客户端错误:" + response.getStatusCode() + ",路径:" + request.getURI());
})
.onStatus(status -> status.is5xxServerError(), (request, response) -> {
throw new RuntimeException("服务端错误:" + response.getStatusCode());
})
.body(String.class);
}
```

View File

@@ -0,0 +1,100 @@
---
title: SpringBoot RocketMQ简介和使用
date: 2026-06-10
---
# 一、简介
&emsp;&emsp;[RocketMQ](https://rocketmq.apache.org/zh/)Apache RocketMQ 是一款面向万亿级消息规模的 AI 原生异步通信引擎。诞生于阿里巴巴高并发电商场景经过数千家企业的生产验证RocketMQ 已从高性能消息队列演进为统一消息平台,横跨传统业务消息、事件流处理和新兴的 AI 原生通信三大范式。
# 二、安装
## 2.1 docker安装
```yml
services:
namesrv:
image: apache/rocketmq:5.3.2
container_name: rmqnamesrv
ports:
- 9876:9876
networks:
- rocketmq
command: sh mqnamesrv
broker:
image: apache/rocketmq:5.3.2
container_name: rmqbroker
ports:
- 10909:10909
- 10911:10911
- 10912:10912
volumes:
- ./broker.conf:/home/rocketmq/rocketmq-5.3.2/conf/broker.conf
environment:
- NAMESRV_ADDR=rmqnamesrv:9876
depends_on:
- namesrv
networks:
- rocketmq
command: sh mqbroker -c /home/rocketmq/rocketmq-5.3.2/conf/broker.conf
proxy:
image: apache/rocketmq:5.3.2
container_name: rmqproxy
networks:
- rocketmq
depends_on:
- broker
- namesrv
ports:
- 8380:8080
- 8381:8081
restart: on-failure
environment:
- NAMESRV_ADDR=rmqnamesrv:9876
command: sh mqproxy
rocketmq-dashboard:
image: apacherocketmq/rocketmq-dashboard:2.1.0
container_name: rocketmq-dashboard
environment:
- JAVA_OPTS=-Drocketmq.namesrv.addr=rmqnamesrv:9876
ports:
- 8382:8082
networks:
- rocketmq
depends_on:
- namesrv
restart: always
networks:
rocketmq:
driver: bridge
```
&emsp;&emsp;broker.conf:
```conf
brokerIP1=192.168.1.100
autoCreateTopicEnable=true
```
::: tip
这里一定要配置brokerIP1否则Broker的地址是docker的内网地址具体可以在控制台中查看Broker的地址。
:::
# 三、SpringBoot使用
## 3.1 引入依赖
```xml
<!-- RocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.3.6</version>
</dependency>
```
::: tip
需要加上版本号SpringBoot3使用2.3以上的版本。
:::
## 3.2 配置文件
```yml
rocketmq:
name-server: 192.168.1.100:9876
producer:
group: seckill-producer-group
```

View File

@@ -304,4 +304,56 @@ public class UserDTO {
@PhoneNumber
private String mobile;
}
```
# 四、ConfigurationProperties注解
&emsp;&emsp;@ConfigurationProperties 是 Spring Boot 提供的核心注解,用于将配置文件(如 application.yml/application.properties中的属性批量绑定到 Java 类的字段上,相比 @Value 注解,它更适合管理一组有层级、有前缀的配置,代码更整洁、可维护性更高。
## 4.1 配置文件
```yml
app:
name: SpringBootDemo
version: 1.0.0
author:
name: 张三
age: 25
servers:
- 192.168.1.100
- 192.168.1.101
- 192.168.1.102
```
## 4.2 配置属性类
```java
@Component
@ConfigurationProperties(prefix = "app")
@Getter
@Setter
public class AppProperties {
// 对应 app.name
private String name;
// 对应 app.version
private String version;
// 嵌套属性 - 对应 app.author
private Author author;
// 集合属性 - 对应 app.servers
private List<String> servers;
@Getter
@Setter
public static class Author {
private String name;
private Integer age;
}
}
```
&emsp;&emsp;属性类支持嵌套和集合属性。
## 4.3 使用
&emsp;&emsp;在Service或相应地方注入即可
```java
@Autowired
private AppProperties appProperties;
```

View File

@@ -0,0 +1,333 @@
---
title: SpringBoot 线程与事件
date: 2026-06-02
---
# 一、基本概念
&emsp;&emsp;**进程**:进程是操作系统资源分配的最小单位,每个进程都有独立的内存空间(代码段、数据段、堆、栈),比如打开一个应用程序,就是一个进程。
&emsp;&emsp;**线程**:线程是 CPU 调度的最小单位,线程共享所属进程的内存空间。 比如一个HTTP请求就是一个线程。
&emsp;&emsp;**协程**:协程是 用户态的轻量级“线程”,由程序自己调度,不依赖操作系统,切换成本极低。
&emsp;&emsp;创建线程方法:
```java
// 继承 Thread 不推荐
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // ✅ 启动线程
}
}
// 实现 Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
public class Test2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
// Lambda 写法
Thread t = new Thread(() -> {
System.out.println("Hello Thread");
});
t.start();
// 线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
```
&emsp;&emsp;**SpringBoot线程池**:
```java
// 1. 启动类加上 @EnableAsync 注解
@EnableAsync
@SpringBootApplication
public class Application {
}
// 2. 线程池配置
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(20);
executor.initialize();
return executor;
}
// 3. 调用方法上加上 @Async 注解
@Service
public class OrderService {
@Async
public void createOrder() {
System.out.println(Thread.currentThread().getName());
}
}
```
# 二、案例
&emsp;&emsp;案例:电商秒杀系统。
- 资源竞争:多个用户抢同一件商品(多线程抢锁)。
- 异步解耦:扣减库存后,需要发短信、发优惠券(多线程并行)。
- 系统解耦:订单系统不应依赖短信系统(事件驱动)。
# 二、JDK原生线程通信
- synchronized锁住库存防止超卖。
- wait/notify通知消息线程“库存已扣可以发通知了”。
- Thread+ ExecutorService实现多线程并行发通知。
## 2.1 共享资源
```java
/**
* 商品库存(共享资源)
*/
public class Stock {
// 假设只有 1 件库存
public int count = 1;
// 信号:是否有人下单成功
public volatile boolean ordered = false;
}
```
## 2.2 下单线程
```java
/**
* 用户下单线程
*/
public class OrderWorker extends Thread {
private final Stock stock;
public OrderWorker(Stock stock) {
this.stock = stock;
}
@Override
public void run() {
// 1. 锁住库存,防止超卖
synchronized (stock) {
if (stock.count > 0) {
System.out.println(Thread.currentThread().getName() + " 抢到锁,准备扣库存");
stock.count--;
stock.ordered = true;
System.out.println("库存扣减成功,剩余:" + stock.count);
stock.notifyAll(); // 通知消息线程
} else {
System.out.println(Thread.currentThread().getName() + " 没抢到,库存不足");
}
}
}
}
```
## 2.3 消息分发线程
```java
/**
* 消息分发线程
* 负责监听下单成功信号,并启动线程池发通知
*/
public class MessageDispatcher extends Thread {
private final Stock stock;
private final ExecutorService pool;
public MessageDispatcher(Stock stock, ExecutorService pool) {
this.stock = stock;
this.pool = pool;
}
@Override
public void run() {
synchronized (stock) {
try {
while (!stock.ordered) {
System.out.println("消息线程:等待下单信号...");
stock.wait(); // 没下单就死等
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("消息线程:收到下单成功信号,开始并行发通知!");
// 2. 并行执行通知任务
pool.execute(() -> System.out.println("线程池任务:发送短信"));
pool.execute(() -> System.out.println("线程池任务:发放优惠券"));
pool.execute(() -> System.out.println("线程池任务:更新用户积分"));
}
}
}
```
## 2.4 主程序
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TraditionalDemo {
public static void main(String[] args) {
Stock stock = new Stock();
ExecutorService pool = Executors.newFixedThreadPool(3);
// 消息线程先启动(等待)
new MessageDispatcher(stock, pool).start();
// 模拟 3 个用户同时下单(竞争)
new OrderWorker(stock).start();
new OrderWorker(stock).start();
new OrderWorker(stock).start();
}
}
```
# 三、SpringBoot 异步方法
- @Transactional:保证库存扣减的事务性。
- @Async:将发短信、发券等耗时操作丢给线程池,立即释放 Tomcat 线程。
## 3.1 配置线程池
```java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("orderExecutor")
public Executor orderExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setThreadNamePrefix("Order-Async-");
executor.initialize();
return executor;
}
}
```
## 3.2 订单服务
```java
@Service
public class OrderService {
@Autowired
private StockService stockService;
@Autowired
private NotifyService notifyService;
/**
* 下单主流程
*/
public void createOrder() {
// 1. 扣库存(核心,同步)
boolean success = stockService.decreaseStock();
if (!success) {
throw new RuntimeException("库存不足");
}
// 2. 异步通知(非核心,并行)
notifyService.sendSms();
notifyService.sendCoupon();
}
}
```
## 3.3 通知服务
```java
@Service
public class NotifyService {
// 指定使用哪个线程池
@Async("orderExecutor")
public void sendSms() {
System.out.println(Thread.currentThread().getName() + " 发送短信");
}
@Async("orderExecutor")
public void sendCoupon() {
System.out.println(Thread.currentThread().getName() + " 发放优惠券");
}
}
```
# 四、SpringBoot 事件机制
- 事件Event下单成功这个“事实”。
- 发布者Publisher只负责发布事件不关心后续。
- 监听者Listener谁关心下单成功谁就干活且可以异步
## 4.1 定义事件
```java
public class OrderCreatedEvent {
private final String orderId;
public OrderCreatedEvent(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
```
## 4.2 发布事件
```java
@Service
public class OrderService {
@Autowired
private StockService stockService;
@Autowired
private ApplicationEventPublisher publisher;
public void createOrder() {
// 1. 扣库存
boolean success = stockService.decreaseStock();
if (success) {
// 2. 发布事件(我不关心谁处理)
publisher.publishEvent(new OrderCreatedEvent("ORDER_123"));
}
}
}
```
## 4.3 监听事件
```java
@Component
public class OrderListener {
// 监听下单事件,并异步执行
@EventListener
@Async("orderExecutor")
public void handleSendSms(OrderCreatedEvent event) {
System.out.println(Thread.currentThread().getName() + " 监听到事件,发送短信");
}
@EventListener
@Async("orderExecutor")
public void handleSendCoupon(OrderCreatedEvent event) {
System.out.println(Thread.currentThread().getName() + " 监听到事件,发放优惠券");
}
}
```

View File

@@ -0,0 +1,154 @@
---
title: SpringBoot WebSocket简介和使用
date: 2026-06-08
---
# 一、简介
&emsp;&emsp;[WebSocket API](https://docs.springjava.cn/spring-framework/reference/web/websocket/server.html)
# 二、安装
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
```
# 三、使用
## 3.1 定义WebSocketHandler
```java
@Component
public class GenericWebSocketHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> SESSION_MAP =
new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
SESSION_MAP.put(userId, session);
log("用户连接成功:" + userId);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String userId = getUserId(session);
SESSION_MAP.remove(userId);
log("用户断开连接:" + userId);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
String payload = message.getPayload();
// 心跳处理
if ("PING".equalsIgnoreCase(payload)) {
session.sendMessage(new TextMessage("PONG"));
return;
}
// 普通消息(可按 JSON 协议扩展)
log("收到消息:" + payload);
session.sendMessage(new TextMessage("服务端已收到:" + payload));
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log("传输异常:" + exception.getMessage());
}
/** 从 session 中获取用户标识(握手阶段放入) */
private String getUserId(WebSocketSession session) {
return (String) session.getAttributes().getOrDefault("userId", session.getId());
}
/** 单发 */
public void sendToUser(String userId, String msg) throws IOException {
WebSocketSession session = SESSION_MAP.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(msg));
}
}
/** 广播 */
public void broadcast(String msg) throws IOException {
for (WebSocketSession session : SESSION_MAP.values()) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(msg));
}
}
}
/** 在线人数 */
public int onlineCount() {
return SESSION_MAP.size();
}
}
```
## 3.2 定义HandshakeInterceptor
```java
@Component
public class GenericHandshakeInterceptor implements HandshakeInterceptor {
/**
* 握手前执行
*/
@Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// 从请求中URL中获取id
// 例如ws://localhost:8080/ws?userId=123多个参数用&连接
String userId = servletRequest.getServletRequest().getParameter("userId");
if (userId == null) {
// 返回 false → 握手失败 → 连接被拒绝
return false;
}
// 放入 attributes 中,方便下次获取
attributes.put("userId", userId);
return true;
}
/**
* 握手完成后执行(一般不用写)
*/
@Override
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {
}
}
```
## 3.3 配置WebSocketConfigurer
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private GenericWebSocketHandler genericWebSocketHandler;
@Resource
private GenericHandshakeInterceptor genericHandshakeInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(genericWebSocketHandler, "/ws")
.addInterceptors(genericHandshakeInterceptor)
.setAllowedOrigins("*");
}
}
```
&emsp;&emsp;WebSocket连接地址ip:port/ws?userId=123。
# 四、前端使用
&emsp;&emsp;参考:[useWebSocket](../Vue/VueUse.md)

202
docs/Web/Vue/ElTable.md Normal file
View File

@@ -0,0 +1,202 @@
---
title: Element Plus 表格组件
date: 2026-05-27
---
# 一、加载中
```vue
<el-table
v-loading="dispatchStore.isLoading"
element-loading-text="加载中..."
:data="dispatchStore.contractList"
border
stripeempty-text="暂无记录"
class="common-table">
</el-table>
```
::: tip
要配合pinia存储使用获取数据前将loading置为true完成后置为false。
:::
# 二、展开行
```vue
<template>
<el-table :data="dispatchStore.contractList"
:row-key="getRowKey"
:expand-row-keys="expandedRows"
@expand-change="handleExpandChange">
<el-table-column type="expand">
<template #default="props">
<el-table :data="props.row.dispatchList"
border stripeempty-text="暂无记录"
class="dispatch-content"
style="width: 100%">
<el-table-column fixed type="index" align="center" width="60" />
<el-table-column prop="customer" label="终端客户" align="center" width="180"/>
</el-table>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
const expandedRows = ref<number[]>([])
// 获取行的唯一标识
const getRowKey = (row: IDispatch) => {
return row.id
}
// 处理展开行变化
const handleExpandChange = (row: IDispatch, expandedRowsList: IDispatch[]) => {
if (expandedRowsList.length > 0) {
// 只保留最新展开的行
expandedRows.value = [getRowKey(row)]
} else {
expandedRows.value = []
}
}
</script>
```
&emsp;&emsp;通过类型expand和插槽props实现可以展示任意格式数据。
&emsp;&emsp;增加表格的`row-key``expand-row-key`属性可以实现只展开某一行功能。
# 三、排序和筛选
```vue
<template>
<el-table :data="dispatchStore.contractList">
<el-table-column prop="contractNum" label="合同编号" align="center" min-width="10%"
:filters=contractNumFilters :filter-method="filterColumnMethod" sortable>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { useDispatchStore } from '@/stores/dispatch.ts'
import { computed } from 'vue'
import type { IContract } from '@/types/dispatch.ts'
const dispatchStore = useDispatchStore()
type ContractField = keyof IContract
const filterColumnMethod = (value, row, column) => {
return row[column.property] === value
}
const getFiltersByKey = (key: ContractField) => {
const result = [...new Set(dispatchStore.contractList.map((item) => item[key]).filter((value) => value != null && value !==''))]
return result.map((item) => ({
text: item,
value: item
}))
}
const contractNumFilters = computed(() => getFiltersByKey('contractNum'))
</script>
```
&emsp;&emsp;`el-table-column`中添加`sortable`即可实现排序功能。
&emsp;&emsp;`el-table-column`中添加`filters``filter-method`实现筛选功能。
&emsp;&emsp;`filters`为原始数据中该字段下的所有值,并转为指定格式,也可以自定义筛选字段。
# 四、表头搜索
```vue
<template>
<el-table :data="dispatchStore.contractList" >
<el-table-column prop="contractNum" label="合同编号" align="center" min-width="10%">
<template #header>
<div class="custom-header">
<span :class="{ 'active': contractNumSearch }">合同编号</span>
<el-popover ref="contractNumPopoverRef" placement="bottom" width="230" trigger="click">
<template #default>
<el-select v-model="contractNumSearch" filterable clearable
placeholder="请选择或输入合同编号" @change="applySearch('contractNum')">
<el-option v-for="(item, index) in contractNumValue"
:key="index" :label="item" :value="item"/>
</el-select>
<div style="margin-top: 8px;display: flex;justify-content: flex-end;">
<el-button type="primary" size="small" @click="applySearch('contractNum')">搜索</el-button>
<el-button size="small" @click="resetSearch('contractNum')">重置</el-button>
</div>
</template>
<template #reference>
<el-icon class="search-icon"><Search/></el-icon>
</template>
</el-popover>
</div>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { useDispatchStore } from '@/stores/dispatch.ts'
import { computed, ref } from 'vue'
import { Search } from '@element-plus/icons-vue'
import type { IContract } from '@/types/dispatch.ts'
import type { PopoverInstance } from 'element-plus'
import type { Ref } from 'vue'
const dispatchStore = useDispatchStore()
type ContractField = keyof IContract
const getFiltersByKey = (key: ContractField) => {
const result = [...new Set(dispatchStore.contractList.map((item) => item[key]).filter((value) => value != null && value !== ''))]
return result.map((item) => ({
text: item,
value: item
}))
}
const contractNumFilters = computed(() => getFiltersByKey('contractNum'))
const contractNumValue = computed(() => contractNumFilters.value.map((item) => item.value))
const contractNumPopoverRef = ref<PopoverInstance>()
const contractNumSearch = ref('')
const popoverMap = new Map<string, Ref<PopoverInstance | undefined>>([
['contractNum', contractNumPopoverRef]
])
const searchMap = new Map<string, Ref<string>>([
['contractNum', contractNumSearch]
])
const applySearch = async (field: ContractField) => {
const searchRef = searchMap.get(field)
if (!searchRef) return
if (!searchRef.value) {
await dispatchStore.queryContractList()
return
}
if (!searchRef.value.trim()) return
dispatchStore.contractList = dispatchStore.contractList.filter((item) => {
const value = item[field]
return typeof value === 'string' && value.includes(searchRef.value)
})
popoverMap.get(field)?.value?.hide()
}
const resetSearch = async (field: string) => {
const searchRef = searchMap.get(field)
if (searchRef) {
searchRef.value = ''
}
popoverMap.get(field)?.value?.hide()
await dispatchStore.queryContractList()
}
</script>
```
&emsp;&emsp;表头搜索主要利用`header`插槽和`el-popover`组件实现。

313
docs/Web/Vue/ElUpload.md Normal file
View File

@@ -0,0 +1,313 @@
---
title: Element Plus 上传器
date: 2026-01-29
---
# 一、上传图片
```vue
<template>
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:on-preview="handlePreview"
:limit="3"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
请上传小于10M的图片
</div>
</template>
<el-dialog v-model="dialogVisible">
<el-image :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</el-upload>
</template>
<script lang="ts" setup>
import { Plus } from '@element-plus/icons-vue'
import { onMounted, ref } from 'vue'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
import { getFileExtension, getFileHashName, validateImage } from '@/utils/file.ts'
import { uploadFileApi } from '@/apis/file.ts'
const imageFileList = ref<UploadUserFile[]>([])
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const baseUrl = ''
const apiImageList = [] as string[]
onMounted(async () => {
imageFileList.value = []
// 根据后端传来的List数据将信息添加文件列表中
apiImageList.forEach((item, index) => {
imageFileList.value.push({
name: index.toString(),
url: `${baseUrl}${item}`
})
})
})
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const maxFileMax = 1024 * 1024 * 10
const fileType = rawFile.type
const fileName = rawFile.name
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
if (!validateImage(fileType, fileExtension)) {
ElMessage.error('请上传图片')
return false
}
if (rawFile.size > maxFileMax) {
ElMessage.error('请上传10M以内的文件!')
return false
}
return true
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
const loading = ElLoading.service({
lock: true,
text: '文件上传中 请等待',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 重命名文件
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
const data = new FormData()
data.append('file', options.file)
data.append('filePath', filePath)
// 调用后端接口上传文件
await uploadFileApi(data)
apiImageList.push(filePath)
ElMessage.success('上传成功')
} catch (error) {
console.log(error)
ElMessage.error('上传失败 请联系管理员')
} finally {
loading.close()
}
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传3张图片')
}
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
const index = imageFileList.value.findIndex((item) => item.uid === uploadFile.uid)
apiImageList.splice(index, 1)
}
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
dialogImageUrl.value = uploadFile.url!
dialogVisible.value = true
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
</script>
```
# 二、上传视频
```vue
<template>
<el-upload
v-model:file-list="videoFileList"
:limit="1"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
请上传小于200M的视频
</div>
</template>
</el-upload>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import type { UploadRequestOptions } from 'element-plus'
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
import { getFileExtension, getFileHashName, validateVideo } from '@/utils/file.ts'
import { uploadFileApi } from '@/apis/file.ts'
const videoFileList = ref<UploadUserFile[]>([])
let baseUrl = ''
const apiVideoUrl = ''
onMounted(async () => {
videoFileList.value = []
if (apiVideoUrl) {
videoFileList.value.push({
name: 'video',
url: `${baseUrl}p/${apiVideoUrl}`
})
}
})
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const maxFileMax = 1024 * 1024 * 200
const fileType = rawFile.type
const fileName = rawFile.name
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
if (!validateVideo(fileType, fileExtension)) {
ElMessage.error('请上传视频')
return false
}
if (rawFile.size > maxFileMax) {
ElMessage.error('请上传200M以内的文件!')
return false
}
return true
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
const loading = ElLoading.service({
lock: true,
text: '文件上传中 请等待',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
const data = new FormData()
data.append('file', options.file)
data.append('filePath', filePath)
await uploadFileApi(data)
baseUrl = filePath
ElMessage.success('上传成功')
} catch (error) {
console.log(error)
ElMessage.error('上传失败 请联系管理员')
} finally {
loading.close()
}
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1个视频')
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
const handleRemove: UploadProps['onRemove'] = () => {
baseUrl = ''
}
</script>
```
# 三、工具类
```ts
import { hashSHA256 } from 'vue3-common/utils/cryptoUtil'
export const getFileHashName = (name: string) => {
return hashSHA256(name)
}
export const getFileExtension = (name: string) => {
return name.substring(name.lastIndexOf('.') + 1).toLowerCase()
}
export const validateExcel = (fileType: string, fileExtension: string): boolean => {
// 常见的 Excel MIME 类型
const excelMimeTypes = [
'application/vnd.ms-excel', // .xls
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
'text/csv' // CSV
]
// 常见的 Excel 文件扩展名
const excelExtensions = ['xls', 'xlsx', 'xlsm', 'csv']
return excelMimeTypes.includes(fileType) || excelExtensions.includes(fileExtension.toLowerCase())
}
export const validateImage = (fileType: string, fileExtension: string): boolean => {
// 常见的图片 MIME 类型
const imageMimeTypes = [
'image/jpeg', // JPEG
'image/jpg', // JPG
'image/png', // PNG
'image/gif', // GIF
'image/bmp', // BMP
'image/webp', // WebP
'image/svg+xml' // SVG
]
// 常见的图片文件扩展名
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
return imageMimeTypes.includes(fileType) || imageExtensions.includes(fileExtension.toLowerCase())
}
export const validateVideo = (fileType: string, fileExtension: string): boolean => {
// 常见的视频 MIME 类型
const videoMimeTypes = [
'video/mp4', // MP4
'video/mpeg', // MPEG
'video/quicktime', // MOV
'video/x-msvideo', // AVI
'video/x-matroska', // MKV
'video/webm', // WebM
'video/3gpp', // 3GP
'video/3gpp2', // 3G2
'video/x-flv', // FLV
'video/mp2t', // TS (MPEG Transport Stream)
'application/x-mpegURL', // M3U8 (HLS)
'video/H264', // H264
'video/H265' // H265/HEVC
]
// 常见的视频文件扩展名
const videoExtensions = [
'mp4',
'mpeg',
'mpg',
'mov',
'avi',
'mkv',
'webm',
'3gp',
'3g2',
'flv',
'ts',
'm3u8',
'h264',
'h265',
'hevc'
]
return videoMimeTypes.includes(fileType) || videoExtensions.includes(fileExtension.toLowerCase())
}
```

View File

@@ -1,5 +1,5 @@
---
title: 前端包管理器
title: Vite核心原理
date: 2025-12-13
---

View File

@@ -0,0 +1,93 @@
---
title: Vue3 defineModel
date: 2026-06-05
---
# 一、概念
&emsp;&emsp;defineModel是对 props + emit的语法糖返回一个可写的 ref。Vu3.4开始支持。
&emsp;&emsp;之前的做法:
```ts
// 子组件
const props = defineProps({
modelValue: Number
})
const emit = defineEmits(['update:modelValue'])
function increment() {
emit('update:modelValue', props.modelValue + 1)
}
```
&emsp;&emsp;现在做法:
```ts
const model = defineModel()
```
# 二、简单示例
&emsp;&emsp;父组件:
```vue
<script setup>
import Stepper from './Stepper.vue'
import { ref } from 'vue'
const quantity = ref(1)
</script>
<template>
<div>
<p>购买数量{{ quantity }}</p>
<Stepper v-model="quantity" />
</div>
</template>
```
&emsp;&emsp;子组件:
```vue
<script setup>
const model = defineModel({
type: Number,
default: 1
})
</script>
<template>
<button @click="quantity--"></button>
<span>{{ quantity }}</span>
<button @click="quantity++">+</button>
</template>
```
&emsp;&emsp;`defineProps`定义类似,可以指定类型和默认值。
# 三、多个v-model
&emsp;&emsp;父组件:
```vue
<UserForm
v-model:name="form.name"
v-model:age="form.age"
/>
```
&emsp;&emsp;子组件:
```vue
<script setup lang="ts">
const name = defineModel<string>('name', {
default: ''
})
const age = defineModel<number>('age', {
default: 0
})
</script>
<template>
<input v-model="name" placeholder="姓名" />
<input v-model="age" type="number" placeholder="年龄" />
</template>
```
::: tip
defineProps: 只读,不能改
defineModel: 可写
:::

View File

@@ -0,0 +1,79 @@
---
title: Vue3自定义指令
date: 2026-06-05
---
# 一、概念
&emsp;&emsp;自定义指令Directive 是 Vue 提供的一种机制,用于直接操作 DOM封装可复用的底层 DOM 行为。它是对模板能力的补充当现有模板语法v-if / v-bind / v-on不够用时就可以用指令。
# 二、简单示例
&emsp;&emsp;注册:
```ts
app.directive('demo', {
mounted(el, binding) {
console.log(el, binding)
}
})
```
&emsp;&emsp;使用:
```vue
<div v-demo="'hello'"></div>
```
&emsp;&emsp;使用该指令,就会触发打印效果。
# 三、生命周期
| 钩子 | 说明 |
| ----- | ----- |
| created | 绑定前 |
| beforeMount | 元素挂载前 |
| mounted | 元素已挂载 |
| beforeUpdate | 更新前 |
| updated | 更新后 |
| beforeUnmount | 卸载前 |
| unmounted | 卸载后 |
# 四、参数
&emsp;&emsp;mounted(el, binding, vnode, prevVnode)
| 参数 | 说明 |
| ----- | ----- |
| el | 当前 DOM 元素 |
| binding.value | 指令绑定的值 |
| binding.arg | 参数 |
| binding.modifiers | 修饰符 |
| vnode | 虚拟节点 |
# 五、封装
&emsp;&emsp;`src`目录新建`directives/**.ts`,定义指令:
```ts
import { useAuthStore } from '@/stores/auth'
export default {
mounted(el, binding) {
const authStore = useAuthStore()
const permission = binding.value
const hasPermission = authStore.currentUser?.permission?.includes(permission)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
}
}
```
&emsp;&emsp;该指令根据用户权限控制DOM是否显示。
&emsp;&emsp;然后在`main.ts`中注册指令:
```ts
import permissionDirective from '@/directives/permission'
app.directive('permission', permissionDirective)
```
&emsp;&emsp;最后在需要的元素中加上`v-permission=""`即可。
::: tip
自定义指令只做DOM相关的事情比如隐藏DOM禁止点击、修改样式等等。
:::

158
docs/Web/Vue/VueUse.md Normal file
View File

@@ -0,0 +1,158 @@
---
title: VueUse简介和使用
date: 2026-06-05
---
# 一、安装
```bash
npm install @vueuse/core
```
# 二、使用
## 2.1 实时获取当前时间
```ts
<template>
<div class="card">
<p>当前时间: {{ formattedTime }}</p>
</div>
</template>
<script setup>
import { useNow, useDateFormat } from '@vueuse/core'
// 获取当前时间响应式对象
const now = useNow()
// 格式化时间
const formattedTime = useDateFormat(now, 'YYYY-MM-DD HH:mm:ss')
</script>
```
## 2.2 useWebSocket
```ts
<script setup lang="ts">
iconst { status, send } = useWebSocket('wss://example.com/ws', {
// 连接成功
onConnected(ws) {
console.log('WS connected')
},
// 收到消息
onMessage(ws, event) {
const msg = JSON.parse(event.data)
console.log('receive:', msg)
},
// 断开
onDisconnected(ws, event) {
console.log('WS disconnected, code:', event.code)
},
onError(ws, event) {
console.error('WS error', event)
},
autoReconnect: {
retries: 3,
delay: 1000
}
})
</script>
```
## 2.3 防抖和节流
| | 防抖 Debounce | 节流 Throttle |
| :--- | :--- | :--- |
| **执行时机** | 连续触发 **停下后** | **每隔一段时间** |
| **执行次数** | 通常 1 次(最后) | 多次,但受限制 |
| **典型场景** | 搜索联想、校验 | 滚动、拖拽 |
&emsp;&emsp;防抖:
```vue
<template>
<input @input="handleInput" placeholder="输入搜索关键字" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const keyword = ref('')
// 输入停止 500ms 后执行搜索
const debouncedSearch = useDebounceFn((val) => {
console.log('发起搜索请求:', val)
// api.search(val)
}, 500)
const handleInput = (e) => {
keyword.value = e.target.value
debouncedSearch(keyword.value)
}
</script>
```
&emsp;&emsp;节流:
```vue
<template>
<div style="padding: 20px">
<p>点击次数被节流{{ count }}</p>
<button @click="throttledClick">
狂点我节流 500ms
</button>
<p style="color:#999;font-size:12px">
快速连点也不会一直 +1 500ms 最多加一次
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useThrottleFn } from '@vueuse/core'
const count = ref(0)
// 节流函数500ms 内最多执行一次
const throttledClick = useThrottleFn(() => {
count.value++
console.log('真正执行 +1')
}, 500)
</script>
```
::: warning
如果是表单提交应该使用状态锁来限制即当提交后将按钮置为loading状态提交成功后再重置。
```vue
<template>
<el-button
type="primary"
:loading="loading"
@click="handleSubmit"
>
{{ loading ? '提交中...' : '提交' }}
</el-button>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const loading = ref(false)
const handleSubmit = async () => {
if (loading.value) return
loading.value = true
try {
// 模拟接口请求
await submitForm()
ElMessage.success('提交成功')
} catch (err) {
ElMessage.error('提交失败')
} finally {
loading.value = false
}
}
const submitForm = () => {
return new Promise(resolve => setTimeout(resolve, 1500))
}
</script>
```
:::

BIN
docs/Web/images/jmeter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,7 +1,7 @@
---
layout: doc
title: Web前端
description: Web前端开发技术文档
title: Web平台
description: Web平台开发技术文档
---
<script setup>

View File

@@ -10,14 +10,14 @@ hero:
alt: VitePress
actions:
- theme: brand
text: Web前端
link: /Web-Front/
text: Web平台
link: /Web/
- theme: brand
text: Web后端
link: /Web-Backend/
- theme: brand
text: DevOps
text: 运维部署
link: /DevOps/
- theme: brand
text: 项目实战
link: /Practice/
features:
- title: 🚀 技术分享

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Some files were not shown because too many files have changed in this diff Show More