feat:增加博客统计组件

This commit is contained in:
2025-12-15 19:53:21 +08:00
parent cbcc48d457
commit 7dfd151119
32 changed files with 1418 additions and 18 deletions

View File

@@ -52,7 +52,8 @@ export default withMermaid({
{ text: 'SpringBoot Starter原理', link: '/Web-Backend/SpringBoot/SpringBootStarter' }, { text: 'SpringBoot Starter原理', link: '/Web-Backend/SpringBoot/SpringBootStarter' },
{ text: 'SpringBoot Bean简介', link: '/Web-Backend/SpringBoot/SpringBootBean' }, { text: 'SpringBoot Bean简介', link: '/Web-Backend/SpringBoot/SpringBootBean' },
{ text: 'SpringBoot3原生镜像', link: '/Web-Backend/SpringBoot/SpringBoot3-GraalVM' }, { text: 'SpringBoot3原生镜像', link: '/Web-Backend/SpringBoot/SpringBoot3-GraalVM' },
{ text: 'SpingBoot技巧', link: '/Web-Backend/SpringBoot/SpingBoot-Skills' } { text: 'SpingBoot技巧', link: '/Web-Backend/SpringBoot/SpingBoot-Skills' },
{ text: 'SpringBoot Common', link: '/Web-Backend/SpringBoot/SpringBoot-Common' }
] ]
}, },
{ {
@@ -125,7 +126,8 @@ export default withMermaid({
{ text: 'SpringBoot Starter原理', link: '/Web-Backend/SpringBoot/SpringBootStarter' }, { text: 'SpringBoot Starter原理', link: '/Web-Backend/SpringBoot/SpringBootStarter' },
{ text: 'SpringBoot Bean简介', link: '/Web-Backend/SpringBoot/SpringBootBean' }, { text: 'SpringBoot Bean简介', link: '/Web-Backend/SpringBoot/SpringBootBean' },
{ text: 'SpringBoot3原生镜像', link: '/Web-Backend/SpringBoot/SpringBoot3-GraalVM' }, { text: 'SpringBoot3原生镜像', link: '/Web-Backend/SpringBoot/SpringBoot3-GraalVM' },
{ text: 'SpingBoot技巧', link: '/Web-Backend/SpringBoot/SpingBoot-Skills' } { text: 'SpingBoot技巧', link: '/Web-Backend/SpringBoot/SpingBoot-Skills' },
{ text: 'SpringBoot Common', link: '/Web-Backend/SpringBoot/SpringBoot-Common' }
] ]
}, },
{ text: 'FastAPI', { text: 'FastAPI',

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import { computed, ref, onMounted } from 'vue'
import { countWord } from '../utils/functions'
const wordCount = ref(0)
const imageCount = ref(0)
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)
})
function analyze() {
document.querySelectorAll('.meta-des').forEach(v => v.remove())
const docDomContainer = window.document.querySelector('#VPContent')
const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>(
'.content-container .main img'
)
imageCount.value = imgs?.length || 0
const words = docDomContainer?.querySelector('.content-container .main')?.textContent || ''
wordCount.value = countWord(words)
}
onMounted(() => {
// 初始化时执行一次
analyze()
})
</script>
<template>
<div class="word">
<p>
<svg t="1724571760788" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6125" width="16" height="16"><path d="M204.8 0h477.866667l273.066666 273.066667v614.4c0 75.093333-61.44 136.533333-136.533333 136.533333H204.8c-75.093333 0-136.533333-61.44-136.533333-136.533333V136.533333C68.266667 61.44 129.706667 0 204.8 0z m307.2 607.573333l68.266667 191.146667c13.653333 27.306667 54.613333 27.306667 61.44 0l102.4-273.066667c6.826667-20.48 0-34.133333-20.48-40.96s-34.133333 0-40.96 13.653334l-68.266667 191.146666-68.266667-191.146666c-13.653333-27.306667-54.613333-27.306667-68.266666 0l-68.266667 191.146666-68.266667-191.146666c-6.826667-13.653333-27.306667-27.306667-47.786666-20.48s-27.306667 27.306667-20.48 47.786666l102.4 273.066667c13.653333 27.306667 54.613333 27.306667 61.44 0l75.093333-191.146667z" fill="#1890FF" p-id="6126"></path><path d="M682.666667 0l273.066666 273.066667h-204.8c-40.96 0-68.266667-27.306667-68.266666-68.266667V0z" fill="#52C41A" p-id="6127"></path></svg>
字数: {{ wordCount }}
<svg t="1724572797268" class="icon" viewBox="0 0 1060 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15031" width="16" height="16"><path d="M556.726857 0.256A493.933714 493.933714 0 0 0 121.929143 258.998857L0 135.021714v350.390857h344.649143L196.205714 334.482286a406.820571 406.820571 0 1 1-15.908571 312.649143H68.937143A505.819429 505.819429 0 1 0 556.726857 0.256z m-79.542857 269.531429v274.907428l249.197714 150.966857 42.422857-70.070857-212.114285-129.389714V269.787429h-79.542857z" fill="#FA8C16" p-id="15032"></path></svg>
时长: {{ readTime }} 分钟
</p>
</div>
</template>
<style>
.word {
color: var(--vp-c-text-2);
font-size: 15px;
}
.icon {
display: inline-block;
transform: translate(0px , 2px);
}
</style>

View File

@@ -1,5 +1,6 @@
import DefaultTheme from 'vitepress/theme' import DefaultTheme from 'vitepress/theme'
import Confetti from "./components/Confetti.vue"; import Confetti from "./components/Confetti.vue";
import ArticleMetadata from "./components/ArticleMetadata.vue"
import type { EnhanceAppContext } from 'vitepress' import type { EnhanceAppContext } from 'vitepress'
import { onMounted, watch, nextTick } from 'vue' import { onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
@@ -27,5 +28,6 @@ export default {
}, },
enhanceApp({ app }: EnhanceAppContext) { enhanceApp({ app }: EnhanceAppContext) {
app.component("Confetti", Confetti); app.component("Confetti", Confetti);
app.component("ArticleMetadata", ArticleMetadata);
}, },
}; };

View File

@@ -0,0 +1,19 @@
const pattern
= /[a-zA-Z0-9_\u0392-\u03C9\u00C0-\u00FF\u0600-\u06FF\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3040-\u309F\uAC00-\uD7AF]+/g
export function countWord(data: string) {
const m = data.match(pattern)
let count = 0
if (!m) {
return 0
}
for (let i = 0; i < m.length; i += 1) {
if (m[i].charCodeAt(0) >= 0x4E00) {
count += m[i].length
}
else {
count += 1
}
}
return count
}

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
## 一、基础概念 ## 一、基础概念
## 二、Docker部署 ## 二、Docker部署

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、Jenkins安装 # 一、Jenkins安装
## 1.1 简介 ## 1.1 简介
&emsp;&emsp;Jenkins 是一款开源的持续集成Continuous Integration, CI 和持续交付Continuous Delivery, CD 自动化工具,广泛应用于软件开发流程中,帮助团队实现代码构建、测试、部署的自动化,从而提升开发效率、减少人为错误,并确保软件质量。 &emsp;&emsp;Jenkins 是一款开源的持续集成Continuous Integration, CI 和持续交付Continuous Delivery, CD 自动化工具,广泛应用于软件开发流程中,帮助团队实现代码构建、测试、部署的自动化,从而提升开发效率、减少人为错误,并确保软件质量。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
## 一、基础概念 ## 一、基础概念
### 1.1 Fluent Bit ### 1.1 Fluent Bit
&emsp;&emsp;[Fluent Bit](https://fluentbit.io/) 是一个开源的、轻量级、高性能的日志处理器和转发器。Fluent Bit 的核心任务是:​​**从各种来源收集日志、指标和追踪数据,进行处理和过滤,然后将其发送到一个或多个目的地。​** &emsp;&emsp;[Fluent Bit](https://fluentbit.io/) 是一个开源的、轻量级、高性能的日志处理器和转发器。Fluent Bit 的核心任务是:​​**从各种来源收集日志、指标和追踪数据,进行处理和过滤,然后将其发送到一个或多个目的地。​**

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、基础语法 # 一、基础语法
## 1.1 变量与常量 ## 1.1 变量与常量
```dart ```dart

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 应用核心Widget # 应用核心Widget
```dart ```dart
// Flutter 应用的入口,配置主题、路由、国际化等。 // Flutter 应用的入口,配置主题、路由、国际化等。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 安卓签名 # 安卓签名
&emsp;&emsp;每次安装/升级软件必须使用同一个签名,否则会将本地数据全部清空。 &emsp;&emsp;每次安装/升级软件必须使用同一个签名,否则会将本地数据全部清空。
## 1. 生成密钥库文件 ## 1. 生成密钥库文件

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、简介 # 一、简介
&emsp;&emsp;[FastAPI](https://fastapi.tiangolo.com/zh/) 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。 &emsp;&emsp;[FastAPI](https://fastapi.tiangolo.com/zh/) 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。
&emsp;&emsp;安装: &emsp;&emsp;安装:

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、基础概念 # 一、基础概念
## 1.1 OAuth2 Password Bearer 模式​ ## 1.1 OAuth2 Password Bearer 模式​
&emsp;&emsp;用于**用户名+密码**登录,获取**access_token**。 &emsp;&emsp;用于**用户名+密码**登录,获取**access_token**。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、简介 # 一、简介
&emsp;&emsp;Flyway 是一个开源的数据库版本控制工具,它极大地简化了数据库的迁移和版本管理。它的核心思想是​​像**用 Git 管理代码一样来管理数据库的结构**​​。 &emsp;&emsp;Flyway 是一个开源的数据库版本控制工具,它极大地简化了数据库的迁移和版本管理。它的核心思想是​​像**用 Git 管理代码一样来管理数据库的结构**​​。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、简介 # 一、简介
&emsp;&emsp;[MyBatis](https://mybatis.org/mybatis-3/)是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJOPlain Old Java Objects普通老式 Java 对象)为数据库中的记录。 &emsp;&emsp;[MyBatis](https://mybatis.org/mybatis-3/)是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJOPlain Old Java Objects普通老式 Java 对象)为数据库中的记录。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# MySQL知识点 # MySQL知识点
## 一、基础知识 ## 一、基础知识
### 1.1 数据类型 ### 1.1 数据类型

View File

@@ -1,36 +1,38 @@
# TCP <ArticleMetadata />
## 定义
&emsp;&emsp;TCPTransmission Control Protocol传输控制协议是互联网核心的面向连接、可靠、字节流的传输层协议工作在 OSI 模型的传输层TCP/IP 模型的传输层),基于 IP 协议提供端到端的可靠数据传输服务。它是 HTTP、HTTPS、WebSocket、MQTT 等应用层协议的底层依赖,核心目标是解决 IP 协议 “无连接、不可靠、无顺序” 的缺陷,确保数据在不可靠的网络中准确、完整、有序地传输。
## 特性 # 一、TCP
### 面向连接 ## 1.1 定义
&emsp;&emsp;TCPTransmission Control Protocol传输控制协议是互联网核心的**面向连接、可靠、字节流**的传输层协议,工作在 OSI 模型的传输层TCP/IP 模型的传输层),基于 IP 协议提供端到端的可靠数据传输服务。它是 HTTP、HTTPS、WebSocket、MQTT 等应用层协议的底层依赖,核心目标是解决 IP 协议 “无连接、不可靠、无顺序” 的缺陷,确保数据在不可靠的网络中准确、完整、有序地传输。
## 1.2 特性
### 1.2.1 面向连接
&emsp;&emsp;通信前必须完成「三次握手」建立连接,通信后通过「四次挥手」释放连接: &emsp;&emsp;通信前必须完成「三次握手」建立连接,通信后通过「四次挥手」释放连接:
- 三次握手:客户端发 SYN → 服务器回 SYN+ACK → 客户端发 ACK确保双方收发能力正常 - 三次握手:客户端发 SYN → 服务器回 SYN+ACK → 客户端发 ACK确保双方收发能力正常
- 四次挥手:客户端发 FIN → 服务器回 ACK → 服务器发 FIN → 客户端回 ACK确保数据传输完毕 - 四次挥手:客户端发 FIN → 服务器回 ACK → 服务器发 FIN → 客户端回 ACK确保数据传输完毕
### 可靠传输 ### 1.2.2 可靠传输
- 序号与确认号:每个字节都有序号,接收方收到后回复确认号,未收到则发送方重传; - 序号与确认号:每个字节都有序号,接收方收到后回复确认号,未收到则发送方重传;
- 超时重传:发送方未在规定时间收到确认,自动重传数据; - 超时重传:发送方未在规定时间收到确认,自动重传数据;
- 流量控制:通过滑动窗口机制,防止发送方发送过快导致接收方缓冲区溢出; - 流量控制:通过滑动窗口机制,防止发送方发送过快导致接收方缓冲区溢出;
- 拥塞控制:通过慢启动、拥塞避免等算法,适应网络带宽变化。 - 拥塞控制:通过慢启动、拥塞避免等算法,适应网络带宽变化。
### 面向字节流 ### 1.2.3 面向字节流
&emsp;&emsp;TCP 将应用层数据视为连续的字节流,不保留应用层数据的边界(与 UDP 的 “数据报” 模式不同): &emsp;&emsp;TCP 将应用层数据视为连续的字节流,不保留应用层数据的边界(与 UDP 的 “数据报” 模式不同):
- 发送方:应用层数据被拆分为 TCP 报文段Segment发送拆分规则由 TCP 协议决定(如 MSS 限制)。 - 发送方:应用层数据被拆分为 TCP 报文段Segment发送拆分规则由 TCP 协议决定(如 MSS 限制)。
- 接收方:将收到的报文段按顺序重组为完整的字节流,再交给应用层,确保数据顺序与发送时一致。 - 接收方:将收到的报文段按顺序重组为完整的字节流,再交给应用层,确保数据顺序与发送时一致。
### 有序传输 ### 1.2.4 有序传输
&emsp;&emsp;TCP 报文段头部包含 “序号Sequence Number” 和 “确认号Acknowledgment Number &emsp;&emsp;TCP 报文段头部包含 “序号Sequence Number” 和 “确认号Acknowledgment Number
- 序号SN标识发送方当前发送的字节流位置如序号为 100 表示当前报文段的第一个字节是整个字节流的第 100 字节)。 - 序号SN标识发送方当前发送的字节流位置如序号为 100 表示当前报文段的第一个字节是整个字节流的第 100 字节)。
- 确认号ACK标识接收方期望下次接收的字节流位置如确认号为 200 表示已正确接收前 199 字节,下次需从 200 字节开始接收)。 - 确认号ACK标识接收方期望下次接收的字节流位置如确认号为 200 表示已正确接收前 199 字节,下次需从 200 字节开始接收)。
- 接收方通过序号排序报文段,丢弃重复报文,确保按发送顺序交付数据。 - 接收方通过序号排序报文段,丢弃重复报文,确保按发送顺序交付数据。
### 全双工通信 ### 1.2.5 全双工通信
&emsp;&emsp;TCP 连接是双向的,双方可同时发送和接收数据,无需等待对方结束发送: &emsp;&emsp;TCP 连接是双向的,双方可同时发送和接收数据,无需等待对方结束发送:
- 每个方向都有独立的发送缓冲区和接收缓冲区,以及独立的滑动窗口用于流量控制。 - 每个方向都有独立的发送缓冲区和接收缓冲区,以及独立的滑动窗口用于流量控制。
- 示例:客户端发送数据的同时,服务器可同步向客户端返回响应,无需等待客户端发送完毕。 - 示例:客户端发送数据的同时,服务器可同步向客户端返回响应,无需等待客户端发送完毕。
## 优缺点 ## 1.3 优缺点
&emsp;&emsp;优点: &emsp;&emsp;优点:
1. 可靠、有序、无丢包 1. 可靠、有序、无丢包
2. 支持流量 / 拥塞控制 2. 支持流量 / 拥塞控制
@@ -41,17 +43,404 @@
2. 头部开销大20-60 字节) 2. 头部开销大20-60 字节)
3. 不适合实时性要求极高的场景(如直播低延迟) 3. 不适合实时性要求极高的场景(如直播低延迟)
## 1.4 Python实现
&emsp;&emsp;TCP服务器
```python
import socket
import signal
import threading
from concurrent.futures import ThreadPoolExecutor
from dotenv import load_dotenv
import os
from model.mqtt import UNKNOWN_MESSAGE
from processor.tcp_processor import TcpMessageProcessor
from config.logger_config import logger
class TCPServer:
def __init__(self):
load_dotenv()
self.host = os.getenv("TCP_HOST", 'localhost')
self.port = int(os.getenv("TCP_PORT", 9100))
self.max_workers = 10
self.timeout = 300
self.server_socket = None
self.running = False
self.thread_pool = ThreadPoolExecutor(max_workers=self.max_workers)
self.message_processor = TcpMessageProcessor()
# 已经连接的客户端
self.clients = {}
self.client_lock = threading.Lock()
self.server_thread = None
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
def _handle_signal(self, signum, frame):
"""处理终止信号,触发优雅关闭"""
logger.info(f"收到信号 {signum},准备关闭服务器...")
self.running = False
def _start_loop(self):
"""启动服务器"""
try:
# 创建 TCP/IP socket AF_INET: IPv4地址族 SOCK_STREAM: TCP协议面向连接
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置SO_REUSEADDR选项允许重用地址和端口避免服务端重启时出现 “地址已被占用” 的错误
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定到指定主机和端口 0.0.0.0 表示所有主机
self.server_socket.bind((self.host, self.port))
# 设置最大等待连接数
self.server_socket.listen(5)
# 设置socket超时时间1.0秒)
self.server_socket.settimeout(1.0)
self.running = True
logger.info(f"TCP服务器启动监听 {self.host}:{self.port} "
f"(最大线程: {self.max_workers}, 超时: {self.timeout}s")
while self.running:
try:
# accept() 会阻塞直到有客户端连接
# client_socket: 与客户端通信的新socket client_address: 客户端地址(ip, port)元组
client_socket, client_address = self.server_socket.accept()
# 获取客户端IP
client_ip = client_address[0]
# 设置客户端socket超时
# 防止客户端长时间不发送数据
client_socket.settimeout(self.timeout)
logger.info(f"新连接: {client_address}")
# 存储客户端
with self.client_lock:
self.clients[client_ip] = client_socket
# 提交到线程池处理
# 提交到线程池是为了让服务器能同时服务多个客户端,而不让一个慢客户端阻塞所有其他客户端
self.thread_pool.submit(self.handle_client, client_socket, client_ip)
except socket.timeout:
continue
except Exception as e:
if self.running:
logger.error(f"接受连接失败: {str(e)}")
except Exception as e:
logger.error(f"TCP服务器启动失败: {str(e)}")
finally:
self.stop()
def start(self):
self.server_thread = threading.Thread(target=self._start_loop, daemon=True)
self.server_thread.start()
def stop(self):
if not self.running:
return
self.running = False
logger.info("开始关闭服务器...")
# 移除客户端
with self.client_lock:
for client_socket in self.clients.values():
try:
client_socket.close()
except Exception as e:
logger.warning(f"关闭客户端连接失败: {str(e)}")
self.clients.clear()
# 关闭线程池
self.thread_pool.shutdown(wait=True)
logger.info("所有客户端处理线程已结束")
# 关闭连接
if self.server_socket:
self.server_socket.close()
logger.info(f"服务器已关闭({self.host}:{self.port}")
def handle_client(self, client_socket, client_ip):
"""处理客户端连接"""
try:
while True:
data = client_socket.recv(1024)
if not data:
logger.info(f"客户端 {client_ip} 主动断开连接")
break
message = data.decode('utf-8').strip()
logger.info(f"收到 {client_ip} 的消息: {message}")
# 放到消息处理器里面处理
response = self.message_processor.process(message)
# 回复消息
if response != UNKNOWN_MESSAGE:
client_socket.sendall(response.encode('utf-8'))
logger.info(f"回复 {client_ip}: {response}")
except socket.timeout:
logger.warning(f"客户端 {client_ip} 超时未活动")
except Exception as e:
logger.error(f"处理 {client_ip} 出错: {str(e)}")
finally:
# 异常情况下关闭连接
with self.client_lock:
if client_ip in self.clients:
del self.clients[client_ip]
try:
client_socket.close()
logger.info(f"客户端 {client_ip} 连接已关闭")
except Exception as e:
logger.warning(f"关闭 {client_ip} 连接失败: {str(e)}")
def send_to_client(self, client_ip, message):
# 先获取客户端连接(加锁保护)
with self.client_lock:
client_socket = self.clients.get(client_ip)
if not client_socket:
logger.warning(f"客户端 {client_ip} 不存在或已断开连接")
return False
# 发送消息
try:
client_socket.sendall(message.encode('utf-8'))
logger.info(f"主动发送消息给 {client_ip}: {message}")
return True
except Exception as e:
logger.error(f"向 {client_ip} 发送消息失败: {str(e)}")
# 发送失败时移除无效连接
with self.client_lock:
if client_ip in self.clients:
del self.clients[client_ip]
return False
tcp_server = TCPServer()
```
&emsp;&emsp;main.py
```python
from endpoint.tcp_server import tcp_server
from config.logger_config import logger
import time
if __name__ == "__main__":
try:
tcp_server.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("收到终止信号,开始关闭程序...")
finally:
tcp_server.stop()
logger.info("程序已退出")
except Exception as e:
logger.critical(f"程序启动失败: {str(e)}", exc_info=True)
exit(1)
```
&emsp;&emsp;启动tcp_server后通过while循环防止主线程退出从而让后台的TCP服务器线程能继续运行。
&emsp;&emsp;在TCPServer中通过while循环持续接收tcp客户端的连接每当有一个客户端连接时会提交到线程池中去处理通过自定义消息处理器将处理完的结果返回给客户端。
&emsp;&emsp;TCP消息处理器
::: code-group
```python [抽象消息处理器]
from abc import ABC, abstractmethod
class AbstractDeviceMessageParser(ABC):
"""设备消息解析器基类"""
@abstractmethod
def check(self, message):
"""判断当前解析器是否能处理该消息"""
pass
@abstractmethod
def parse(self, message):
"""解析消息内容"""
pass
@abstractmethod
def response(self, parsed_data):
"""根据解析后的数据生成响应"""
pass
```
```python [示例消息处理器]
from datetime import datetime
from model.band import BandData
from model.mqtt import MqttTopic, MqttData, UNKNOWN_MESSAGE
from parser.abstract_parsers import AbstractDeviceMessageParser
import re
from utils.index import str_length_to_4hex
from config.logger_config import logger
from endpoint.mqtt_client import mqtt_client
class JuweiBandParser(AbstractDeviceMessageParser):
"""聚伟手环消息解析器"""
def __init__(self):
# 聚伟手环消息格式: [MNYD*设备ID*内容长度*内容]
self.pattern = r'MNYD'
self.vendor = "聚伟手环"
self.tag = "MNYD"
self.device_id = ""
def check(self, message):
"""检查是否为聚伟手环的消息格式"""
return re.search(self.pattern, message) is not None
def parse(self, message):
"""解析聚伟手环消息"""
try:
parts = message.strip("[]").split("*")
self.device_id = parts[1]
content = parts[3]
return {
'vendor': self.vendor,
'device_id': self.device_id,
'content': content,
}
except Exception as e:
logger.error(f"解析{self.vendor}消息出错: {str(e)}")
return None
def publish_vital_data(self, item, value):
mqtt_client.publish(
topic=MqttTopic.JuWei_Band_Post.value,
payload=MqttData(
deviceIp="",
deviceId=self.device_id,
payload=BandData(item=item, value=value).model_dump_json(),
).model_dump_json())
def response(self, parsed_data):
"""生成聚伟手环的响应消息"""
if not parsed_data:
return UNKNOWN_MESSAGE
# 处理不同命令
content = parsed_data['content']
parts = content.split(",", 1)
content_tag = parts[0] if len(parts) > 0 else ""
content_value = parts[1] if len(parts) > 1 else ""
replay = UNKNOWN_MESSAGE
logger.info(f"解析{content_tag}消息")
match content_tag:
# PING消息 [MNYD*334588000000156*0004*PING]
case "PING":
replay = "PING,1"
# 日期,步数,翻滚次数,电量百分数,里程数(km)
# [MNYD*334588000000156*0014*KA,120414,50,100,100,100.12]
case "KA":
replay = content_tag
# 位置数据上报
# case "UD":
# replay = ""
# 报警数据上报
# [MNYD*334588000000156*00CD*AL,180916,064153,A,22.570512,N,113.8623267,
# E,0.00,154.8,0.0,11,100,100,0,0,00100018,7,0,460,1,9529,
# 21809,155,9529,21242,132,9529,21405,131,9529,63554,131,9529,
# 63555,130,9529,63556,118,9529,21869,116,0,12.4]
case "AL":
replay = content_tag
# 获取服务器端时间
# [MNYD*YYYYYYYYYYYYYYY*LEN*LGZONE]
case "LGZONE":
now = datetime.now()
current_date = now.date().strftime("%Y-%m-%d")
current_time = now.time().strftime("%H:%M:%S")
replay = f"{content_tag},+8,{current_time},{current_date}"
# 请求位置数据 TODO
case "WG":
replay = content_tag
# 请求电话本设置信息 TODO
case "PHLQ":
replay = "PHL"
# 请求SOS设置信息 TODO
case "SOS":
replay = content_tag
# 终端心率上传
case "heart":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传体温数据 [MNYD*334588000000156*0009*temp,36.2]
case "temp":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传血压数据 [MNYD*334588000000156*000C*blood,150,70]
case "blood":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传血氧数据 [MNYD*334588000000156*0009*oxygen,97]
case "oxygen":
self.publish_vital_data(content_tag, content_value)
replay = content_tag
# 上传睡眠数据报告
case "SLEEPRPT":
replay = "SLEEP"
case _:
logger.info("不需要回复")
return UNKNOWN_MESSAGE
return f"[{self.tag}*{parsed_data['device_id']}*{str_length_to_4hex(replay)}*{replay}]"
```
```python [消息处理器]
from model.mqtt import UNKNOWN_MESSAGE
from parser.juwei_band_parser import JuweiBandParser
from config.logger_config import logger
class TcpMessageProcessor:
"""消息处理器,负责将消息路由到正确的设备解析器"""
def __init__(self):
# 注册所有支持的设备解析器
self.parsers = [JuweiBandParser()]
def process(self, message):
"""处理消息,返回响应"""
# 尝试找到能处理该消息的解析器
for parser in self.parsers:
if parser.check(message):
parsed_data = parser.parse(message)
if parsed_data:
logger.info(f"处理{parsed_data['vendor']}消息: {message}")
return parser.response(parsed_data)
# 没有找到合适的解析器
logger.warning(f"未识别的消息格式: {message}")
return UNKNOWN_MESSAGE
```
:::
# HTTP # HTTP
## 定义 ## 定义
&emsp;&emsp;HTTPHyperText Transfer Protocol超文本传输协议是互联网的核心协议之一用于客户端如浏览器、App与服务器之间的通信是万维网WWW数据交换的基础。它定义了请求 / 响应的格式、传输规则和状态码等核心机制,支持从简单文本到复杂多媒体(图片、视频、文件)的传输,也是现代 Web 应用的底层通信标准。 &emsp;&emsp;HTTPHyperText Transfer Protocol超文本传输协议是互联网的核心协议之一用于**客户端如浏览器、App与服务器之间的通信**是万维网WWW数据交换的基础。它定义了请求 / 响应的格式、传输规则和状态码等核心机制,支持从简单文本到复杂多媒体(图片、视频、文件)的传输,也是现代 Web 应用的底层通信标准。
## 特性 ## 特性
### 请求 - 响应模式 ### 请求 - 响应模式
&emsp;&emsp;通信由客户端主动发起请求服务器接收后处理并返回响应不存在服务器主动向客户端推送数据的情况HTTP/2 引入 Server Push 扩展,可主动推送关联资源)。 &emsp;&emsp;通信由客户端主动发起请求服务器接收后处理并返回响应不存在服务器主动向客户端推送数据的情况HTTP/2 引入 Server Push 扩展,可主动推送关联资源)。
&emsp;&emsp;一次完整通信流程:客户端建立连接 → 发送请求 → 服务器处理 → 返回响应 → 连接关闭HTTP/1.1 默认开启长连接 Keep-Alive &emsp;&emsp;一次完整通信流程:**客户端建立连接 → 发送请求 → 服务器处理 → 返回响应 → 连接关闭**HTTP/1.1 默认开启长连接 Keep-Alive
### 无状态 ### 无状态
&emsp;&emsp;服务器不会保存客户端的会话状态(如登录状态、浏览记录),每次请求都是独立的,服务器无法通过协议本身识别连续请求是否来自同一客户端。通过 Cookie、Session、Token如 JWT等机制补充状态管理。 &emsp;&emsp;**服务器不会保存客户端的会话状态**(如登录状态、浏览记录),每次请求都是独立的,服务器无法通过协议本身识别连续请求是否来自同一客户端。通过 Cookie、Session、Token如 JWT等机制补充状态管理。
## 版本 ## 版本
### HTTP/1.01996 年) ### HTTP/1.01996 年)
@@ -159,8 +548,8 @@ Set-Cookie: sessionId=abc123; Path=/
# WebSocket # WebSocket
## 定义 ## 定义
&emsp;&emsp;它的核心特点是:一旦客户端与服务器建立连接,双方就可以在这个连接上实时、双向地发送数据,无需像 HTTP 那样每次通信都由客户端发起请求,非常适合实时通信场景(如聊天、直播弹幕、实时数据监控、在线协作等) &emsp;&emsp;WebSocket 是一种**全双工、双向、持久化的网络通信协议**(属于应用层协议),由 HTML5 规范定义,专门解决 HTTP 协议无法实现服务器主动向客户端推送数据的问题
&emsp;&emsp;它的核心特点是:**一旦客户端与服务器建立连接,双方就可以在这个连接上实时、双向地发送数据**,无需像 HTTP 那样每次通信都由客户端发起请求,非常适合实时通信场景(如聊天、直播弹幕、实时数据监控、在线协作等)。
| 特性 | HTTP | WebSocket | | 特性 | HTTP | WebSocket |
|---------------------|-------------------------------|-------------------------------| |---------------------|-------------------------------|-------------------------------|
@@ -209,7 +598,7 @@ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 服务器加密后的密
# MQTT # MQTT
## 定义 ## 定义
&emsp;&emsp;MQTTMessage Queuing Telemetry Transport消息队列遥测传输是一种**轻量级、低带宽、低功耗的发布 / 订阅Publish/Subscribe模式物联网IoT通信协议**,由 IBM 于 1999 年设计,核心目标是解决受限设备(如传感器、嵌入式设备)和低带宽、不稳定网络环境下的高效数据传输问题。
## 架构 ## 架构
- 发布者Publisher发送消息的设备 / 服务; - 发布者Publisher发送消息的设备 / 服务;

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 简介 # 简介
&emsp;&emsp;[RustFS](https://rustfs.com.cn/) 是一个基于 Rust 构建的高性能分布式对象存储系统。 &emsp;&emsp;[RustFS](https://rustfs.com.cn/) 是一个基于 Rust 构建的高性能分布式对象存储系统。
&emsp;&emsp;具体以下特点: &emsp;&emsp;具体以下特点:

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、JsonView # 一、JsonView
&emsp;&emsp;JsonView 是 Jackson 提供的注解,用于控制对象序列化/反序列化时包含哪些字段。可以实现: &emsp;&emsp;JsonView 是 Jackson 提供的注解,用于控制对象序列化/反序列化时包含哪些字段。可以实现:
- 不同接口返回不同字段 - 不同接口返回不同字段

View File

@@ -0,0 +1,871 @@
<ArticleMetadata />
# 一、框架说明
&emsp;&emsp;本框架基于Spring Boot3框架二次开发增加了依赖包管理和启动项配置等功能。
# 二、项目结构
| 项目模块 | 模块含义 | 主要功能 |
| :-----------: | :-------------------: | :---------------------------------: |
| starters | SpringBoot 启动项 | 主要包括web、jdbc和log启动项配置 |
| autoconfigure | staters具体启动配置类 | 主要包括web和jdbc具体的启动项配置类 |
| dependencies | 依赖项 | 主要包括本框架中的依赖包管理 |
| framework | 通用配置 | 主要包括web和data的一些通用工具方法 |
# 三、项目说明
## 3.1 common模块
&emsp;&emsp;该模块主要声明项目结构包括autoconfigure、dependencies、framework、starter-parent和starters等模块。
```xml
<modules>
<!-- modules表示聚合关系即common有以下模块 -->
<module>dependencies</module>
<module>starters</module>
<module>framework</module>
<module>autoconfigure</module>
<module>starter-parent</module>
</modules>
```
## 3.2 dependencies模块
&emsp;&emsp;该模块为其他模块的父模块,声明了一些常用依赖包及版本,通过`<dependencyManagement>`管理,只声明依赖的版本,并不会实际引入依赖。
&emsp;&emsp;后续有新增依赖时需要先在dependencies模块声明版本然后在相应的starter模块中增加依赖。
```xml
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<spring.boot.version>3.5.0</spring.boot.version>
<springdoc.version>2.7.0</springdoc.version>
<logback-more-appenders.version>1.8.8</logback-more-appenders.version>
<fluency-fluentd.version>2.7.0</fluency-fluentd.version>
<mybatis-spring.version>3.0.4</mybatis-spring.version>
<mybatis.plus.version>3.5.12</mybatis.plus.version>
<sa.token.version>1.43.0</sa.token.version>
<hutool.version>5.8.26</hutool.version>
<commons.io.version>2.19.0</commons.io.version>
<commons.collections.version>4.4</commons.collections.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
<commons.net.version>3.9.0</commons.net.version>
<guava.version>31.1-jre</guava.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<yitter.idgenerator.version>1.0.6</yitter.idgenerator.version>
<lombok.version>1.18.30</lombok.version>
<lombok.mapstruct.version>0.2.0</lombok.mapstruct.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<docker-maven-plugin.version>0.41.0</docker-maven-plugin.version>
<native-maven-plugin.version>0.10.2</native-maven-plugin.version>
</properties>
<!-- dependencyManagement部分只声明依赖的版本并不会实际引入依赖。
需要在具体的模块中显式声明依赖,才能让模块使用这些库 -->
<dependencyManagement>
<dependencies>
<!-- SpringBoot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringDoc OpenAPI + Swagger UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</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>
<!-- SaToken BOM -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-bom</artifactId>
<version>${sa.token.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- hutool BOM -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<!-- MyBatis-Plus Maven BOM -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis.plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- IO工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<!-- 集合工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons.collections.version}</version>
</dependency>
<!-- 字符串工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- 网络工具类 -->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons.net.version}</version>
</dependency>
<!-- 工具类集合、缓存、并发、IO、字符串 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-bom</artifactId>
<version>${guava.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 实体映射工具类 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- 雪花ID生成器 -->
<dependency>
<groupId>com.github.yitter</groupId>
<artifactId>yitter-idgenerator</artifactId>
<version>${yitter.idgenerator.version}</version>
</dependency>
<!-- starter中的依赖 -->
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-logging</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-web</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-jdbc</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker-maven-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-maven-plugin.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
```
::: tip
dependencyManagement部分只声明依赖的版本并不会实际引入依赖。
需要在具体的模块中显式声明依赖,才能让模块使用这些库
:::
## 3.3 framework模块
&emsp;&emsp;该模块主要是一些通用的配置。
### 3.3.1 data
&emsp;&emsp;主要提供了2个数据库实体类的基类。
::: code-group
```java [AbstractIdEntity]
@Getter
@Setter
public abstract class AbstractIdEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
}
```
```java [AbstractEntity]
@Getter
@Setter
public abstract class AbstractEntity extends AbstractIdEntity {
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(value = "create_by", fill = FieldFill.INSERT)
private String createBy;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
private String updateBy;
}
```
:::
### 3.3.2 web
&emsp;&emsp;主要提供了自定义异常处理类。
::: code-group
```java [ErrorResponse]
/**
* 错误响应
*/
public final class ErrorResponse {
/**
* 自定义code
*/
private final String code;
/**
* 自定义消息
*/
private final String message;
public ErrorResponse(String message) {
this.code = "Unspecified";
this.message = message;
}
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
```
```java [CustomException]
/**
* 自定义异常类
*/
public class CustomException extends AbstractException {
public CustomException(String errorMessage) {
super(errorMessage);
}
public CustomException(String code, String errorMessage) {
super(code, errorMessage);
}
public CustomException(String errorMessage, Exception innerException) {
super(errorMessage, innerException);
}
public CustomException(String code, String errorMessage, Exception innerException) {
super(code, errorMessage, innerException);
}
}
```
```java [AbstractException]
/**
* 抽象异常类
*/
public abstract class AbstractException extends RuntimeException {
protected String code;
public AbstractException(String errorMessage) {
super(errorMessage);
}
public AbstractException(String code, String errorMessage) {
super(errorMessage);
this.code = code;
}
public AbstractException(String errorMessage, Exception innerException) {
super(errorMessage, innerException);
}
public AbstractException(String code, String errorMessage, Exception innerException) {
super(errorMessage, innerException);
this.code = code;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
```
:::
### 3.4 starters模块
&emsp;&emsp;该模块为整个项目的核心模块在实际项目中通过引入相应的starter模块并配合autoconfigure模块中的功能即可实现快速自动装配。
&emsp;&emsp;该模块的pom.xml声明的依赖会注入到实际的项目中。
&emsp;&emsp;在springboot3中可以在**main->resources->META-INF->spring->org.springframework.boot.autoconfigure.AutoConfiguration.imports**中声明需要自动执行的类。
#### 3.4.1 stater-jdbc
&emsp;&emsp;该模块主要提供数据库相关的配置功能。
&emsp;&emsp;目前实现的功能有:
1. 配置MybatisPlus拦截器添加乐观锁和分页插件。
2. 自定义MybatisPlus ID生成器雪花ID
3. 配置MybatisPlus自动填充字段create_time、create_by、update_time和update_by
::: code-group
```java [JdbcAutoConfiguration]
/**
* jdbc自动配置
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SqlSessionFactory.class)
@Import({YitterGenerator.class, MybatisMetaObjectHandler.class})
public class JdbcAutoConfiguration {
/**
* MybatisPlus拦截器
* @return 拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
```
```java [YitterGenerator]
/**
* 雪花id生成器
*/
public class YitterGenerator implements IdentifierGenerator {
@Override
public Number nextId(Object entity) {
return YitIdHelper.nextId();
}
}
```
```java [MybatisMetaObjectHandler]
/**
* Mybatis Plus自动填充
*/
public class MybatisMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, Constants.CREATE_TIME_FLAG, LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, Constants.UPDATE_TIME_FLAG, LocalDateTime.class, LocalDateTime.now());
if (StpUtil.isLogin()) {
this.strictInsertFill(metaObject, Constants.CREATE_BY_FLAG, String.class, StpUtil.getLoginIdAsString());
this.strictInsertFill(metaObject, Constants.UPDATE_BY_FLAG, String.class, StpUtil.getLoginIdAsString());
}
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, Constants.UPDATE_TIME_FLAG, LocalDateTime.class, LocalDateTime.now());
if (StpUtil.isLogin()) {
this.strictUpdateFill(metaObject, Constants.UPDATE_BY_FLAG, String.class, StpUtil.getLoginIdAsString());
}
}
}
```
:::
&emsp;&emsp;`@ConditionalOnClass(SqlSessionFactory.class)`表示只有存在SqlSessionFactory类时这个配置类才会生效。即如果没有 MyBatis 相关依赖,这个配置类会被 Spring 完全忽略。
&emsp;&emsp;`@Import`用于导入其他配置类或组件到当前配置类中。
#### 3.4.2 starter-web
&emsp;&emsp;该模块主要提供web相关的配置功能。
&emsp;&emsp;目前实现的功能有:
1. 注册审计拦截器
2. 注册Sa-Token拦截器
3. 注册安全拦截器
4. 配置CORS跨越
5. 全局异常处理器
::: code-group
```java [ServerAutoConfiguration]
@Configuration()
@Import({DefaultExceptionAdvice.class, AuditBodyAdvice.class, CustomProperties.class})
public class ServerAutoConfiguration implements WebMvcConfigurer {
@Resource
private CustomProperties customProperties;
/**
* 注册拦截器 需要实现 WebMvcConfigurer 接口
* @param registry 注册器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册安全拦截器
// registry.addInterceptor(new SecurityInterceptor());
// 注册审计拦截器
registry.addInterceptor(new AuditInterceptor(customProperties.getBasePackage()));
// 注册Sa-Token拦截器 登录校验
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.excludePathPatterns("/error", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**")
.excludePathPatterns("/files/**")
.excludePathPatterns("/doc.html", "/webjars/**");
}
/**
* 配置CORS跨越 需要实现 WebMvcConfigurer 接口
* @return 过滤器
*/
@Bean
@Order(-128)
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有来源
config.addAllowedOrigin("*");
// 允许所有请求头
config.addAllowedHeader("*");
// 允许所有请求方法
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
```
```java [AuditInterceptor]
public class AuditInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(AuditInterceptor.class);
private final String basePackage;
public AuditInterceptor(String basePackage) {
this.basePackage = basePackage;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果是Http请求
if (handler instanceof HandlerMethod hd) {
request.setAttribute(Constants.AUDIT_TIME_FLAG, System.currentTimeMillis());
// 判断该请求是否需要审计
if (WebUtils.checkIsAuditPackages(hd.getBeanType().getPackage(), basePackage)) {
handleAuditPackageRequest(request);
}
}
// true表示继续处理请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 这里我们不需要处理时间计算,所有的计算在 afterCompletion 中完成
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 获取开始时间
Long startTime = (Long) request.getAttribute(Constants.AUDIT_TIME_FLAG);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
logger.info("<AuditSummary> Request URL {} | Time Taken {} ms", request.getRequestURI(), duration);
}
}
/**
* 处理审计包请求
* @param request 请求
*/
private void handleAuditPackageRequest(HttpServletRequest request) {
if (StringUtils.isEmpty(request.getQueryString())) {
logger.info("<AuditSummary> {} {}", request.getMethod(), request.getRequestURI());
} else {
logger.info("<AuditSummary> {} {}?{}", request.getMethod(), request.getRequestURI(), WebUtils.format2UTF8(request.getQueryString()));
}
}
}
```
```java [DefaultExceptionAdvice]
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class DefaultExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionAdvice.class);
/**
* 处理SaToken权限错误
* @param e 权限异常
* @return 错误响应
*/
@ExceptionHandler(SaTokenException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleSaTokenException(SaTokenException e) {
ErrorResponse response = new ErrorResponse(String.valueOf(e.getCode()), e.getMessage());
logger.error("<{}> {}", response.getCode(), e.toString());
return response;
}
/**
* 处理参数校验错误
* @param e 校验异常
* @return 错误响应
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse handleValidationException(MethodArgumentNotValidException e) {
ErrorResponse response = new ErrorResponse(WebUtils.formatValidationException(e.getBindingResult().getFieldErrors()));
logger.error("<{}> {}", response.getCode(), response.getMessage());
return response;
}
/**
* 处理自定义异常错误
* @param e 自定义异常
* @return 错误响应
*/
@ExceptionHandler(AbstractException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleAbstractException(AbstractException e) {
ErrorResponse response;
if (e.getCode() == null || e.getCode().isEmpty()) {
response = new ErrorResponse(e.getMessage());
} else {
response = new ErrorResponse(e.getCode(), e.getMessage());
}
logger.error(String.format("<%s> ", response.getCode()), e);
return response;
}
/**
* 处理默认异常错误
* @param e 默认异常
* @return 错误响应
*/
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleDefaultException(Exception e) {
ErrorResponse response = new ErrorResponse(e.getMessage());
logger.error(String.format("<%s> ", response.getCode()), e);
return response;
}
}
```
```java [AuditBodyAdvice]
@ControllerAdvice
@Import({CustomProperties.class})
public class AuditBodyAdvice implements RequestBodyAdvice, ResponseBodyAdvice<Object> {
@Resource
private CustomProperties customProperties;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(AuditBodyAdvice.class);
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 判断是否为需要审计的包
String auditPackages = customProperties.getBasePackage();
return WebUtils.checkIsAuditPackages(methodParameter.getDeclaringClass().getPackage(), auditPackages)
&& AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 入参结束
try {
String jsonBody = objectMapper.writeValueAsString(body);
logger.info("<AuditRequest> {}", jsonBody);
} catch (JsonProcessingException e) {
logger.info("<AuditResponse> {}", body);
}
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 判断是否为需要审计的包
String auditPackages = customProperties.getBasePackage();
return WebUtils.checkIsAuditPackages(returnType.getDeclaringClass().getPackage(), auditPackages)
&& AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 出参结束
if (body != null && !(body instanceof ErrorResponse)) {
try {
String jsonBody = objectMapper.writeValueAsString(body);
logger.info("<AuditResponse> {}", jsonBody);
} catch (JsonProcessingException e) {
logger.info("<AuditResponse> {}", body);
}
}
return body;
}
}
```
```java [CustomProperties]
/**
* 用于将配置文件(如 application.properties 或 application.yml中的属性值绑定到 Java 对象
*/
@ConfigurationProperties(prefix = "web-starter")
public class CustomProperties {
// yml中web-starter下的base-package字段值
private String basePackage = "";
public String getBasePackage() {
return basePackage;
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
}
```
:::
&emsp;&emsp;`@ConfigurationProperties`用于将配置文件(如 application.properties 或 application.yml中的属性值绑定到 Java 对象
::: tip
SpringBoot MVC总体执行顺序
1. 进入Tomcat容器
2. 进入Filter过滤器
3. 进入Servlet容器
4. 进入Interceptor拦截器
5. 进入Controller控制器
6. 进入AOP
:::
::: tip
请求和响应体拦截器
RequestBodyAdvice, ResponseBodyAdvice主要发生在Controller执行前后
1. preHandle():请求处理前
2. beforeBodyRead请求体反序列化前
3. @RequestBodyController方法参数绑定
4. Controller请求处理
5. beforeBodyWrite():响应体序列化之前
6. postHandle():请求处理后
7. 视图渲染
8. afterCompletion():请求结束
:::
::: tip
审计拦截器 可以实现请求日志打印等功能
HandlerInterceptor 拦截器执行顺序:
1. preHandle():请求处理前 按注册顺序依次执行。
2. Controller请求处理 请求到达Controller并被处理。
3. postHandle():请求处理后,视图渲染前 按注册顺序逆序执行。
4. afterCompletion():视图渲染后 按注册顺序逆序执行。
:::
# 四、项目发布
## 4.1 发布到Git中
1. 在Gitee/Github中创建工程需要在工程中创建一个文件夹例如repo后续发布的文件要放在该文件夹下。
2. 在maven的setting.xml中添加server和repository信息
```xml
<servers>
<server>
<id>gitee</id>
<username>Cxx0822</username>
<password>token</password>
</server>
</servers>
<profiles>
<profile>
<id>gitee</id>
<repositories>
<repository>
<id>central</id>
<url>https://maven.aliyun.com/repository/central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>gitee</id>
<url>https://gitee.com/Cxx0822/springboot2-common/raw/master/repo</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<url>https://maven.aliyun.com/repository/central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>gitee</activeProfile>
</activeProfiles>
```
&emsp;&emsp;注这里要去掉mirror的阿里云镜像。
3. 在工程的根目录的pom.xml中添加发布配置
```xml
<distributionManagement>
<repository>
<id>gitee</id>
<name>springboot2-common</name>
<url>file:D:/temp/maven</url>
</repository>
</distributionManagement>
```
&emsp;&emsp;注gitee不支持通过deploy发布jar包可以先发布到本地再将文件复制到项目文件夹下通过git push推送。
4. 将本地产生的发布文件上传至git仓库中。
5. 其他项目引用:
```xml
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-web</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-jdbc</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-logging</artifactId>
<version>1.0.0</version>
</dependency>
```
# 五、项目使用
1. 将父工程改为starter-parent模块
```xml
<parent>
<groupId>com.cxx</groupId>
<artifactId>starter-parent</artifactId>
<version>2.0.0</version>
<relativePath/>
</parent>
```
2. 根据需要引入starter-web、starter-jdbc和starter-logging模块
```xml
<dependencies>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-web</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-jdbc</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.cxx</groupId>
<artifactId>starter-logging</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
```
3. 打包模块时需要添加spring-boot-maven-plugin
```xml
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
```

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、启动流程 # 一、启动流程
![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/7d7b8c086729d400a3891a20f2107fa6.png 'SpringBoot启动流程.png') ![暂无图片](https://cxx0822.iepose.cn/rustfs/blog/7d7b8c086729d400a3891a20f2107fa6.png 'SpringBoot启动流程.png')

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、原生镜像 # 一、原生镜像
&emsp;&emsp;传统 Java 应用基于 JVM 运行,需要加载完整的类库和 JVM 运行时环境,导致启动时间长、内存占用高。而**原生镜像技术通过提前编译AOT将 Java 应用直接编译为本地机器码,无需 JVM 即可运行**,具有以下核心优势: &emsp;&emsp;传统 Java 应用基于 JVM 运行,需要加载完整的类库和 JVM 运行时环境,导致启动时间长、内存占用高。而**原生镜像技术通过提前编译AOT将 Java 应用直接编译为本地机器码,无需 JVM 即可运行**,具有以下核心优势:

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、Bean概念 # 一、Bean概念
## 1.1 定义 ## 1.1 定义
&emsp;&emsp;Spring bean是Spring框架在运行时管理的对象Bean是一个由Spring IoC容器实例化、组装和管理的对象。 &emsp;&emsp;Spring bean是Spring框架在运行时管理的对象Bean是一个由Spring IoC容器实例化、组装和管理的对象。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、简介 # 一、简介
&emsp;&emsp;Spring Boot Starter是一组预定义的依赖项集合旨在简化Maven或Gradle等构建工具中的依赖管理。每个Starter都包含了实现特定功能所需的库和组件以及相应的配置文件。开发者只需在项目中引入相应的Starter依赖即可快速搭建起具备该功能的项目骨架。 &emsp;&emsp;Spring Boot Starter是一组预定义的依赖项集合旨在简化Maven或Gradle等构建工具中的依赖管理。每个Starter都包含了实现特定功能所需的库和组件以及相应的配置文件。开发者只需在项目中引入相应的Starter依赖即可快速搭建起具备该功能的项目骨架。
&emsp;&emsp;Starter=依赖+自动配置+配置文件 &emsp;&emsp;Starter=依赖+自动配置+配置文件

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、IOC # 一、IOC
&emsp;&emsp;Spring框架的核心是IOC控制反转容器它负责管理应用程序中的对象称为Bean的创建、配置和组装。 &emsp;&emsp;Spring框架的核心是IOC控制反转容器它负责管理应用程序中的对象称为Bean的创建、配置和组装。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、基础概念 # 一、基础概念
&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 中的一部分,它是一个基于 **<font style="color:#DF2A3F;">请求驱动</font>** 的 Web 框架,主要用于构建 Web 应用程序,并且遵循 MVC模型-视图-控制器) 设计模式。它将 Web 应用的业务逻辑、用户界面和请求处理分离,使得代码更加模块化和可维护。

View File

@@ -18,6 +18,7 @@ SpringBoot 框架学习系列:
- [SpringBoot Bean简介](/Web-Backend/SpringBoot/SpringBootBean) - [SpringBoot Bean简介](/Web-Backend/SpringBoot/SpringBootBean)
- [SpringBoot3原生镜像](/Web-Backend/SpringBoot/SpringBoot3-GraalVM) - [SpringBoot3原生镜像](/Web-Backend/SpringBoot/SpringBoot3-GraalVM)
- [SpingBoot技巧](/Web-Backend/SpringBoot/SpingBoot-Skills) - [SpingBoot技巧](/Web-Backend/SpringBoot/SpingBoot-Skills)
- [SpringBoot Common](/Web-Backend/SpringBoot/SpringBoot-Common)
### 🐍 FastAPI ### 🐍 FastAPI
Python FastAPI 框架学习: Python FastAPI 框架学习:

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、网络请求阶段 # 一、网络请求阶段
1. DNS 解析 1. DNS 解析

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# JavaScript简介 # JavaScript简介
## JavaScript 实现 ## JavaScript 实现
&emsp;&emsp;完整的`JavaScript`实现包含以下几个部分: &emsp;&emsp;完整的`JavaScript`实现包含以下几个部分:

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# npm # npm
&emsp;&emsp;2010年随Node.js发布的npm首次为JavaScript引入了标准的包管理系统。其核心创新是**package.json文件**,通过语义化版本规范定义了依赖声明标准。这一设计解决了手动管理依赖时的版本混乱问题,为模块化开发提供了基础设施。 &emsp;&emsp;2010年随Node.js发布的npm首次为JavaScript引入了标准的包管理系统。其核心创新是**package.json文件**,通过语义化版本规范定义了依赖声明标准。这一设计解决了手动管理依赖时的版本混乱问题,为模块化开发提供了基础设施。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、Vite简介 # 一、Vite简介
&emsp;&emsp;Vite是新一代的前端构建工具在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+ Webpack-dev-server。 &emsp;&emsp;Vite是新一代的前端构建工具在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+ Webpack-dev-server。
&emsp;&emsp;其主要利用浏览器**ESModule特性**导入组织代码在服务器端按需编译返回完全跳过了打包这个概念服务器随起随用。生产中利用Rollup作为打包工具号称下一代的前端构建工具。 &emsp;&emsp;其主要利用浏览器**ESModule特性**导入组织代码在服务器端按需编译返回完全跳过了打包这个概念服务器随起随用。生产中利用Rollup作为打包工具号称下一代的前端构建工具。

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、Vue3 整体架构图 # 一、Vue3 整体架构图
```mermaid ```mermaid
graph TD graph TD

View File

@@ -1,3 +1,5 @@
<ArticleMetadata />
# 一、依赖管理 # 一、依赖管理
&emsp;&emsp;本项目会传递安装的依赖有: &emsp;&emsp;本项目会传递安装的依赖有: