feat:增加项目实战模块

This commit is contained in:
2026-05-27 11:26:16 +08:00
parent 8c9b3adc57
commit efc1f5fbae
15 changed files with 687 additions and 318 deletions

View File

@@ -32,7 +32,8 @@ export default withMermaid({
sidebar: {
'/Web/': [routers[0]],
'/DevOps/': [routers[1]],
'/Others/': [routers[2]]
'/Practice/': [routers[2]],
'/Others/': [routers[3]]
},
socialLinks: [

View File

@@ -6,7 +6,13 @@ export const routers = [
text: '🌿 Vue',
items: [
{ text: 'Vue3-Common', link: '/Web/Vue/Vue3-Common' },
{ text: 'Vue3架构原理', link: '/Web/Vue/Vue3-Architecture' }
{ text: 'Vue3架构原理', link: '/Web/Vue/Vue3-Architecture' },
{ text: 'JavaScript知识点整理', link: '/Web/Vue/JavaScript-Guide' },
{ text: '前端包管理器', link: '/Web/Vue/Package' },
{ text: 'Vite核心原理', link: '/Web/Vue/Vite-Principle' },
{ text: '浏览器渲染全流程解析', link: '/Web/Vue/Browser-Process' },
{ text: 'Element Plus 上传器', link: '/Web/Vue/ElUpload' },
{ text: 'Element Plus 表格组件', link: '/Web/Vue/ElTable' },
]
},
{
@@ -43,6 +49,14 @@ export const routers = [
{ text: 'SQL高阶用法', link: '/Web/MySQL/SQL-Advance' }
]
},
{
text: '📶 Network',
items: [
{ text: '网络编程简介', link: '/Web/Network/Summary' },
{ text: '.Net Core实现', link: '/Web/Network/NetCore' },
{ text: 'Mqtt简介与使用', link: '/Web/Network/Mqtt' }
]
},
{
text: '📱 Flutter',
items: [
@@ -55,11 +69,6 @@ export const routers = [
{
text: '🔄 其他',
items: [
{ text: 'JavaScript知识点整理', link: '/Web/Others/JavaScript-Guide' },
{ text: '前端包管理器', link: '/Web/Others/Package' },
{ text: 'Vite核心原理', link: '/Web/Others/Vite-Principle' },
{ text: '浏览器渲染全流程解析', link: '/Web/Others/Browser-Process' },
{ text: 'Element Plus 上传器', link: '/Web/Others/ElUpload' },
{ text: 'RustFS简介和使用', link: '/Web/Others/RustFS' },
{ text: 'AList简介和使用', link: '/Web/Others/AList' },
{ text: 'InfluxDB简介和使用', link: '/Web/Others/InfluxDB' },
@@ -79,6 +88,22 @@ export const routers = [
{ text: 'Linux命令', link: '/DevOps/LinuxCommand' }
]
},
{
text: '🏗️ 项目实战',
items: [
{
text: '🏢 公司项目',
items: [
{ text: '埃学习', link: '/Practice/Company/Learn' }
]
},
{
text: '👤 个人项目',
items: [
]
},
]
},
{
text: '🔄 其他',
items: [

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[3] />

View File

@@ -0,0 +1,68 @@
---
title: 埃学习
date: 2026-05-27
---
# 一、项目背景
&emsp;&emsp;为解决企业内部线下培训成本高、进度难追踪的问题,开发了一套企业在线学习平台,用于员工课程学习、在线考试、培训计划管理与数据统计分析,已全面接入企业微信工作台。
# 二、系统架构
# 三、核心模块
## 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 |
| **存储** | MySQL 8.0 (业务数据)、AList (视频/文件资源存储) |
| **视频处理** | Java JAVE 2 (FFmpeg 封装,视频压缩转码) |
| **运维** | Docker、Fluent Bit (日志采集)、OpenObserver (日志可视化) |
# 五、难点/亮点
## 5.1 公网视频播放卡顿
* 问题
&emsp;&emsp;服务器公网带宽小,高清视频并发播放严重卡顿。
* 方案
&emsp;&emsp;上传视频后使用JAVE2调用FFmpeg将视频统一压缩为720p的清晰度视频。
* 效果
&emsp;&emsp;视频体积减少60%+,公网环境播放流畅。
## 5.2 学习进度防作弊
* 问题
&emsp;&emsp;员工学习视频课程时,可通过拖动进度条快速完成课程,无法真实反映学习行为,存在“刷课”风险。
* 方案
&emsp;&emsp;根据管理员设定的课程学习时长,在员工学习时,启动定时器;每分钟会保存学习时长至数据库中,下次学习时,直接跳转至上次学习进度;课程是否完成已定时器结束为准。
* 效果
&emsp;&emsp;有效防止通过拖拽进度条刷课,保证学习时长真实可信;同时支持断点续学,避免重复学习,满足企业培训考核与合规审计要求。
## 5.3 学习计划通知
* 问题
&emsp;&emsp;培训计划、考试通知无法及时触达员工,导致培训参与率低、管理成本高。
* 方案
&emsp;&emsp;在发布培训计划时,调用企业微信消息推送接口,自动向指定员工发送应用消息通知;结合定时任务,对未完成培训的人员进行周期性提醒。
* 效果
&emsp;&emsp;培训通知实时触达,未读提醒显著提高培训完成率;管理员无需手动维护人员信息,降低了系统运维成本。
# 六、成果
1. 平台全面替代线下培训,年度培训计划完成率由 60% 提升至 95% 以上,满足了企业合规培训要求。
2. 过视频压缩方案解决了公网带宽瓶颈,结合企业微信自动化通知,大幅降低人工催办成本,整体培训运营成本下降约 40%。
3. 实现了学习进度防作弊与学习记录全跟踪,为公司提供精准的数据统计报表,支撑人才盘点与绩效评估。

10
docs/Practice/index.md Normal file
View File

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

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/+将无法匹配到消息
:::

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

@@ -0,0 +1,311 @@
---
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>();
// 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
.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) : SuperSocketService<ProtocolFrame>(serviceProvider, serverOptions)
{
}
```
# 三、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");
}
}
```
# 四、 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

@@ -634,310 +634,3 @@ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 服务器加密后的密
| 固定头部 | 必选2 字节起包含消息类型如发布、订阅、QoS 等级、是否保留消息等标识。 |
| 可变头部 | 可选,仅部分消息类型(如发布、订阅)需要,包含主题名、消息 ID 等信息。 |
| 负载 | 可选,消息的实际内容(如 JSON 字符串、二进制数据),例如 {"temperature": 25}。 |
# 五、.Net Core实现
## 5.1 SuperSocket
&emsp;&emsp;[SuperSocket](https://www.supersocket.net/)是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。可以轻松构建TCP、UDP、WebSocket服务器。
## 5.2 安装依赖
&emsp;&emsp;NuGut安装SuperSocket、SuperSocket.WebSocket和SuperSocket.WebSocket.Server 2.0及以上版本。
## 5.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"
]
}
}
```
## 5.3 主程序
&emsp;&emsp;program.cs
```cs
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddSingleton<IPackageHandler<ProtocolFrame>, TcpPackageHandler>();
services.AddSingleton<WebSocketMessageHandler>();
// 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
.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()可以构造多服务器实例。
## 5.4 TCP服务器
### 5.4.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;可以根据实际情况自定义消息格式。
### 5.4.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来解包。
### 5.4.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);
}
}
```
### 5.4.4 TCPService
```cs
class TcpService(IServiceProvider serviceProvider, IOptions<ServerOptions> serverOptions) : SuperSocketService<ProtocolFrame>(serviceProvider, serverOptions)
{
}
```
## 5.5 Websocket服务器
### 5.5.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");
}
}
```
## 5.6 MQTT服务器
### 5.6 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;需要实现后台服务接口一直运行。

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`组件实现。

View File

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

View File

@@ -13,8 +13,11 @@ hero:
text: Web平台
link: /Web/
- theme: brand
text: DevOps
text: 运维部署
link: /DevOps/
- theme: brand
text: 项目实战
link: /Practice/
features:
- title: 🚀 技术分享