搜索结果

×

搜索结果将在这里显示。

🍉使用示例(VB.NET)

安装方式 参考 快速入门

默认端口8090,传入参数则监听指定端口,Windows下注意赋予管理员权限

推荐使用 PicoServer 最新版本,保障功能完整性

完整的示例代码,直接复制粘贴即可运行并验证

1.最简 WebAPI

Private ReadOnly MyAPI As New WebAPIServer '实例化PicoServer

Sub Main()
    MyAPI.AddRoute("/", AddressOf Hello) '添加根路由映射
    MyAPI.StartServer()
    Console.WriteLine("http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer() '停止服务
End Sub

'根路由映射的方法
Private Async Function Hello(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Await response.WriteAsync("Hello PicoServer")
End Function

相关方法

MyAPI.AddRoute("/", AddressOf Hello) '路由映射,不限制请求方法
MyAPI.AddRoute("/", AddressOf Hello, "GET") '路由映射,限定为 GET 方法
MyAPI.StartServer '开启服务
MyAPI.StopServer  '停止服务
response.WriteAsync("Hello PicoServer") '写入文本响应(UTF-8编码)
request.HttpMethod '获取请求方法,可根据此实现同一个路由对不同请求方法进行不同处理

2.路由和参数解析

路由有三种风格。

  1. 精准路由
  2. 星号路由
  3. RESTful 风格路由(文档后面)
Private ReadOnly MyAPI As New WebAPIServer

Sub Main()
    ' 注册精确路由(优先级高于通配符路由)
    MyAPI.AddRoute("/api/user/query", AddressOf QueryUser, "GET")
    MyAPI.AddRoute("/api/user/save", AddressOf SaveUser, "POST")
    MyAPI.AddRoute("/api/user/json", AddressOf SaveUserJson, "POST")

    ' 注册星号通配符路由(每段 URL 仅支持一个 *,支持多段通配)
    MyAPI.AddRoute("/api/*/posts", AddressOf HandleWildcardPost, "POST")
    MyAPI.AddRoute("/api/*/user/*/detail", AddressOf HandleMultiWildcard, "GET")

    ' 启动服务
    MyAPI.StartServer()
    Console.WriteLine("服务已启动 http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

'处理 GET 查询参数请求
'路由:/api/user/query
Private Async Function QueryUser(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim name As String = request.GetQuery("name")
    Dim age As Integer = request.GetQuery(Of Integer)("age")
    Dim isVip As Boolean = request.GetQuery(Of Boolean)("isVip")

    Dim output = $"{{
        ""code"": 1,
        ""msg"": ""参数解析成功"",
        ""data"": {{ 
            ""name"": ""{name}"", 
            ""age"": {age}, 
            ""isVip"": {isVip} 
        }}
    }}"
    Await response.WriteAsync(output)
End Function

'处理 POST 表单数据请求
'路由:/api/user/save
'Content-Type: application/x-www-form-urlencoded
Private Async Function SaveUser(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim formData = request.ParseForm()
    Dim userName As String = formData("userName")
    Dim phone As String = formData("phone")
    Await response.WriteAsync($"{{""code"":1, ""msg"":""表单保存成功"",""data"":{{""userName"":""{userName}"",""phone"":""{phone}""}}}}")
End Function

'处理 POST JSON 数据请求
'路由: /api/user/json
'Content-Type: application/json
Private Async Function SaveUserJson(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim bodyJson As String = Await request.ReadBodyAsStringAsync()
    Await response.WriteAsync($"{{""code"":1, ""msg"":""JSON 保存成功"",""data"":{bodyJson}}}")
End Function

'处理单层星号通配符 POST 请求
'路由:POST /api/*/posts(匹配 /api/xxx/posts,* 为任意单段路径)
'内置防目录遍历,自动拦截 ../ 等非法字符
Private Async Function HandleWildcardPost(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim requestUrl As String = request.Url.AbsolutePath
    Dim bodyJson As String = Await request.ReadBodyAsStringAsync()
    Dim output = $"{{
        ""code"": 1,
        ""msg"": ""通配符路由匹配成功"",
        ""data"": {{
            ""requestUrl"": ""{requestUrl}"",
            ""receivedData"": {bodyJson}
        }}
    }}"
    Await response.WriteAsync(output)
End Function

'处理多层星号通配符 GET 请求
' 路由:GET /api/*/user/*/detail(匹配 /api/xxx/user/yyy/detail,每段一个 *)
Private Async Function HandleMultiWildcard(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim requestUrl As String = request.Url.AbsolutePath
    Dim output = $"{{""code"":1,""msg"":""多层通配符匹配成功"",""requestUrl"":""{requestUrl}""}}"
    Await response.WriteAsync(output)
End Function

相关方法

request.GetQuery()'获取指定查询参数,不存在则返回nothing
request.GetQuery(Of T) '按类型返回指定查询参数,失败返回类型默认值
request.ParseForm() '获取表单数据
request.ReadBodyAsStringAsync() '获取 body 字符串

3.Cookie 增删改查

Sub Main()
    MyAPI.AddRoute("/cookie/set", AddressOf SetCookie, "GET")
    MyAPI.AddRoute("/cookie/get", AddressOf GetCookie, "GET")
    MyAPI.AddRoute("/cookie/delete", AddressOf DeleteCookie, "GET")
    MyAPI.AddRoute("/cookie/clear", AddressOf ClearCookies, "GET")

    MyAPI.StartServer(8090)
    Console.WriteLine("Cookie 测试服务已启动:http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

' 设置 Cookie(支持过期时间、路径、HttpOnly)
Private Async Function SetCookie(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    ' 添加普通 Cookie(1小时过期)
    response.AppendCookie("token", "pico_1234567", New CookieOptions With {
            .Expires = DateTimeOffset.Now.AddHours(1),
            .Path = "/",
            .HttpOnly = True ' 防止前端 JS 读取,提升安全性
        })
    ' 添加自定义路径 Cookie
    response.AppendCookie("theme", "dark", New CookieOptions With {
            .Expires = DateTimeOffset.Now.AddDays(7),
            .Path = "/"
        })
    response.BuildCookie() '关键,多个Cookie需进行拼接
    Await response.WriteAsync("{""code"":1, ""msg"":""Cookie 设置成功""}")
End Function

' 读取 Cookie
Private Async Function GetCookie(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    ' 安全读取 Cookie(避免空引用)
    Dim token As String = Nothing
    request.TryGetCookieValue("token", token)
    Dim theme As String = Nothing
    request.TryGetCookieValue("theme", theme)

    Await response.WriteAsync($"{{""code"":1, ""token"":""{token}"",""theme"":""{theme}""}}")
End Function

' 删除指定 Cookie
Private Async Function DeleteCookie(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    response.DeleteCookie("token", New CookieOptions With {.Path = "/"})
    Await response.WriteAsync("{""code"":1, ""msg"":""Token Cookie 删除成功""}")
End Function

' 批量清理所有 Cookie
Private Async Function ClearCookies(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    response.ClearCookies()
    Await response.WriteAsync("{""code"":1, ""msg"":""所有 Cookie 已清理""}")
End Function

4.RESTful 风格路由

Private ReadOnly MyAPI As New WebAPIServer()

Sub Main()
    MyAPI.AddRoute("/users", AddressOf Users)
    MyAPI.StartServer()
    Console.WriteLine("服务已启动:http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

Private Async Function Users(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    response.ContentType = GetContentType(".json")
    Select Case request.HttpMethod
        Case "GET"
            Await response.WriteAsync("{""code"":1,""msg"":""获取用户成功"",""data"":{""username"":""PicoServer""}}")
        Case "POST"
            response.StatusCode = 201
            Await response.WriteAsync("{""code"":1,""msg"":""创建用户成功"",""data"":{""userId"":""123456""}}")

        Case Else
            response.StatusCode = 405
            Await response.WriteAsync("{""code"":0,""msg"":""方法不允许""}")
    End Select
End Function

相关方法

GetContentType(".html") '根据文件扩展名获取 MIME 类型:text/html;charset=UTF-8

GetContentType 为跨平台通用方法(Windows/Linux/Docker 结果一致),可根据文件扩展名获取标准化 MIME 类型,支持的扩展名有:.html/.htm、.css、.js、.json、.txt、.xml、.jpg/.jpeg、.png、.gif、.webp、.svg/.svgz、.ico、.mp4、.webm、.mp3、.wav、.m3u8、.ts、.woff2、.woff、.ttf、.otf、.pdf、.zip、.rar、.7z、.apk;文本类类型默认带 UTF-8 编码,未知类型返回 application/octet-stream。

5.静态文件托管

静态文件(HTML/CSS/JS/ 图片 / 视频)托管配置,适配前端页面直接访问、静态资源服务等场景。如B/S架构的网站应用

Friend ReadOnly MyAPI As New WebAPIServer

Sub Main()
    '添加静态文件服务, wwwroot 目录  /api/ 路径 同时兼容 Web 前端页面和 WebAPI 接口
    MyAPI.AddStaticFiles("/", "wwwroot", "/api/")
    MyAPI.AddCors() '允许跨域
    MyAPI.StartServer()
    '保持程序作为服务运行,兼容windows和linux
    Console.WriteLine("服务器已启动,按Ctrl+C退出...")
    Thread.Sleep(Timeout.Infinite)
End Sub

相关函数

'添加静态文件服务。第一个参数为路由,第二个为服务端文件夹(相对/绝对路径),第三个为排除的API路径,专为B/S架构添加,用于同时兼容 Web 前端页面和 WebAPI 接口
MyAPI.AddStaticFiles("/", "wwwroot", "/api/")
MyAPI.AddCors() '启用跨域

6.跨域配置

解决前后端分离跨域限制,支持极简配置与自定义配置

MyAPI.AddCors() '启用跨域,默认允许所有来源/方法/请求头
MyAPI.AddCors("picoserver.cn") '支持指定跨域
'更多个性化跨域自定义跨域中间件即可。

7.文件上传/下载

Private ReadOnly MyAPI As New WebAPIServer()

Sub Main()
    MyAPI.AddRoute("/file/download", AddressOf DownloadFile, "GET")    ' 文件下载/预览
    MyAPI.AddRoute("/file/upload", AddressOf UploadFile, "POST")       ' 文件上传

    MyAPI.StartServer()
    Console.WriteLine("文件服务已启动:http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

' 文件下载/预览(asAttachment=false 预览,true 强制下载)
Private Async Function DownloadFile(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim TestFile As String = "D:\test.mp4"
    If Not File.Exists(TestFile) Then
        Await response.WriteAsync("{""code"":0, ""msg"":""文件不存在""}")
        Return
    End If
    Await response.SendFileAsync(TestFile, True)
End Function

' 文件上传(带进度回调)
Private Async Function UploadFile(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim fname As String = request.Headers("filename") '从请求头中获取文件名(test112.mp4),示例为相对位置,中文文件名建议用BASE64编码或者URL编码
    Dim isSuccess As Boolean = Await request.SaveFileAsync(fname, Sub(current, total)' 打印上传进度(百分比)                                                              Console.WriteLine($"上传进度:{ current * 100 / total}%")
    End Sub)

    If isSuccess Then
        Await response.WriteAsync("{""code"":1, ""msg"":""文件上传成功""}")
    Else
        Await response.WriteAsync("{""code"":0, ""msg"":""文件上传失败""}")
    End If
    Console.WriteLine($"请求头中的文件名:{fname}")
End Function

相关函数

'发送文件,支持断点下载
response.SendFileAsync()'流式发送,大文件低内存消耗,根据扩展名自动添加文件类型
response.SendFileAsync(filePath)'支持文档/视频等直接预览
response.SendFileAsync(filePath,true)'强制下载
response.SendFileAsync(Mp4Path,false,request)'播放视频,支持拖动播放
'接受文件上传,支持断点续传
request.SaveFileAsync()'流式保存,大文件低内存消耗
request.SaveFileAsync(filePath)'保存文件到指定路径(相对/绝对皆可),需要包含文件名
request.SaveFileAsync(filePath,onProgress)'保存文件到指定路径,支持回调进度。

request.Headers("filename") '举例:从请求头中获取文件名,生产中应进行非空判断

8.流媒体/直播流推送

Private ReadOnly MyAPI As New WebAPIServer()

Sub Main()
    MyAPI.AddRoute("/stream/live", AddressOf LiveStream, "GET") ' 直播流推送
    MyAPI.StartServer()
    Console.WriteLine("流媒体服务已启动:http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

Private Async Function LiveStream(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim LiveFile As String = "D:\test.mp4" ' 直播源文件(可替换为实时流)
    If Not File.Exists(LiveFile) Then
        Await response.WriteAsync("{""code"":0, ""msg"":""直播源不存在""}")
        Return
    End If

    ' 打开文件流(FileShare.ReadWrite 允许文件被其他程序写入)
    Using fs As New FileStream(LiveFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
        Await response.SendStreamAsync(fs, GetContentType(".mp4"), True)
    End Using
End Function

相关方法

response.SendStreamAsync(fs, GetContentType(".mp4"), True)'无缓存、Chunked 传输,适配实时流,各种流

9.简单 Token/JWT 鉴权

一旦添加鉴权,默认所有路由都需要鉴权,不需要鉴权的路由需要添加到路由白名单

白名单只针对精准路由(路径),不支持星号路由

路由白名单

MyAPI.RouteWhiteList '储存路由白名单的集合
MyAPI.RouteWhiteList.Add("/api/login") '添加接口到白名单,无需验证

简单token鉴权

Private ReadOnly MyAPI As New WebAPIServer()
Private _testToken As String = "PicoServer123" ' 测试用 Token

Sub Main()
    MyAPI.AddRoute("/api/login", AddressOf Login, "POST")
    MyAPI.AddRoute("/api/user/info", AddressOf GetUserInfo, "GET")

    '配置白名单(放行登录接口)
    MyAPI.RouteWhiteList.Add("/api/login")
    MyAPI.AddSimpleTokenVerify(_testToken)

    MyAPI.StartServer()
    Console.WriteLine($"鉴权服务已启动,测试 Token:{_testToken}")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

' 登录接口(白名单,无需鉴权)
Private Async Function Login(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Await response.WriteAsync($"{{""code"":1, ""msg"":""登录成功"",""token"":""{_testToken}""}}")
End Function

' 用户信息接口(需鉴权,非白名单)
Private Async Function GetUserInfo(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Dim token As String = request.GetToken()
    Await response.WriteAsync($"{{""code"":1, ""msg"":""获取信息成功"",""token"":{token}}}")
End Function

相关方法

MyAPI.AddSimpleTokenVerify(testToken) '添加简单 token 验证中间件,参数为token
request.GetToken() '获取请求头中的token值

JWT鉴权

Private ReadOnly MyAPI As New WebAPIServer()
Private _testToken As String ' 测试用 Token

Sub Main()
    ' 1. 注册路由(先注册,后配置鉴权)
    MyAPI.AddRoute("/api/login", AddressOf Login, "POST")       ' 白名单路由(无需鉴权)
    MyAPI.AddRoute("/api/user/info", AddressOf GetUserInfo, "GET") ' 需鉴权路由

    ' 2. 配置白名单(放行登录接口)
    MyAPI.RouteWhiteList.Add("/api/login")

    ' 3. 启用 JWT 鉴权(密钥需与生成 Token 时一致)
    MyAPI.AddJwtTokenVerify("pico_secret_779")

    ' 4. 生成测试 Token(模拟登录成功)
    GenerateTestToken()

    MyAPI.StartServer()
    Console.WriteLine($"鉴权服务已启动,测试 Token:{_testToken}")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

' 简化版:生成测试 JWT Token
Private Sub GenerateTestToken()
    ' 过期时间:当前+1小时(10位时间戳)
    Dim exp As Long = MyAPI.GetTimeStamp10(3600)
    ' 直接使用字符串插值构造载荷,简化代码
    Dim payload As String = $"{{""username"":""admin"",""role"":""super"",""exp"":{exp}}}"
    ' 生成 Token
    _testToken = MyAPI.Jwt.GenerateToken(payload)
End Sub

' 登录接口(白名单,无需鉴权)
Private Async Function Login(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    ' 模拟登录验证(实际场景替换为账号密码校验)
    Await response.WriteAsync($"{{""code"":1, ""msg"":""登录成功"",""token"":""{_testToken}""}}")
End Function

' 用户信息接口(需鉴权,非白名单)
Private Async Function GetUserInfo(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    ' 获取请求头中的 Token 并解析载荷
    Dim token As String = request.GetToken()
    Dim payload As String = MyAPI.Jwt.DecodePayload(token)

    Await response.WriteAsync($"{{""code"":1, ""msg"":""获取信息成功"",""userInfo"":{payload}}}")
End Function

相关方法

MyAPI.AddJwtTokenVerify("pico_secret_779")'添加 JWT 鉴权中间件,参数为HS256加密密钥

request.GetToken() '获取请求头中的token值
MyAPI.Jwt.DecodePayload(token)'解码 JWT 负载为字符串
MyAPI.Jwt.GenerateToken(payload) '创建 JWT token 参数为负载,用于储存信息

MyAPI.GetTimeStamp10(3600) ' 获取10位时间戳,参数:需要追加的秒数,不传入则返回当前时间戳
MyAPI.GetTimeStamp13() ' 获取13位时间戳,参数:需要追加的毫秒数,不传入则返回当前时间戳

10.长连接消息推送

Private ReadOnly MyAPI As New WebAPIServer()

Sub Main()
    MyAPI.AddRoute("/notify", AddressOf LongConnectionPush, "GET") ' 长连接推送
    MyAPI.StartServer(8090)
    Console.WriteLine("长连接服务已启动:http://127.0.0.1:8090/notify")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

' 长连接消息推送(模拟设备报警)
Private Async Function LongConnectionPush(request As HttpListenerRequest, response As HttpListenerResponse) As Task
    Try
        ' 循环推送消息(实际场景替换为业务事件触发)
        For i As Integer = 0 To 29
            ' 推送报警消息(低内存)
            Await response.WriteChunkAsync($"设备报警 {i}:温度异常 {DateTime.Now:HH:mm:ss}" & vbCrLf)
            ' 模拟 1 秒推送一次
            Await Task.Delay(1000)
        Next

        ' 推送结束,发送结束标识
        Await response.WriteChunkAsync("推送结束")
    Finally
        ' 必须关闭响应
        response.Close()
    End Try
End Function

相关方法

response.WriteChunkAsync("推送结束") '推送字符串
response.Close() '关闭响应,释放资源 【WriteChunkAsync下必须手动关闭,避免资源泄露】

11.WebSocket 通信

  • 场景目标:实现 WebSocket 双向交互,适配实时聊天、设备指令交互等场景

  • 核心要点:服务端(enableWebSocket 启用、事件订阅、在线客户端管理、广播消息)、客户端(WebSocketClient 初始化、事件订阅、消息发送)

  • 运行测试:服务端启动、客户端连接、双向消息交互、广播消息接收

WebSocket 服务端可以和 WebAPI 同时存在,且共用端口,共用地址

WebSocket 服务端

Private ReadOnly MyAPI As New WebAPIServer()

Sub Main()
    MyAPI.enableWebSocket = True
    MyAPI.WsOnConnectionChanged = AddressOf WsConnectChanged
    MyAPI.WsOnMessage = AddressOf OnMessageReceived

    MyAPI.StartServer()
    Console.WriteLine("PicoServer WebSocket:http://127.0.0.1:8090")
    Console.ReadKey()
    MyAPI.StopServer()
End Sub

Private Async Function WsConnectChanged(clientId As String, connected As Boolean) As Task
    Await MyAPI.WsBroadcastAsync($"{clientId} {connected}")
End Function

Private Async Function OnMessageReceived(clientId As String, message As String, reply As Func(Of String, Task)) As Task
    Await reply("收到!")
    Dim clients = MyAPI.WsGetOnlineClients
    For Each client In clients
        Await MyAPI.WsSendToClientAsync(client, $"{clientId}说:{message}")
    Next
End Function

相关方法

MyAPI.enableWebSocket = True '启用WebSocket支持
MyAPI.WsOnConnectionChanged ' 事件:WebSocket客户端连接状态发生变化
MyAPI.WsOnMessage '事件:收到WebSocket客户端发送来的消息
MyAPI.WsBroadcastAsync() '对所有在线客户端广播消息
MyAPI.WsGetOnlineClients '获取在线客户端列表
MyAPI.WsSendToClientAsync(client,message) '给指定客户端发送消息
MyAPI.WsEnableHeartbeat = True '启用 WebSocket 服务端心跳检测,默认false
MyAPI.WsHeartbeatTimeout = 60 '设置 WebSocket 心跳时间,默认30秒
MyAPI.WsMaxConnections = 200 '设置 WebSocket 最大连接数,默认100
MyAPI.WsPingString = "hi" '设置 WebSocket 的ping消息,默认"ping",不区分大小写

WebSocket 客户端

Private wsClient As New WebSocketClient("wss://echo.websocket.org/")

Sub Main()
    ' 订阅核心事件
    AddHandler wsClient.OnConnected, AddressOf OnConnected ' 连接成功
    AddHandler wsClient.OnMessageReceived, AddressOf OnMessageReceived ' 接收消息
    AddHandler wsClient.OnDisconnected, AddressOf OnDisconnected ' 连接断开
    AddHandler wsClient.OnError, AddressOf OnError ' 异常触发
    wsClient.StartConnect()
    Console.ReadKey()
    wsClient.StopConnect()
End Sub

Private Sub OnConnected(sender As Object, e As EventArgs)
    Console.WriteLine("连接成功!")
End Sub

Private Sub OnMessageReceived(sender As Object, message As String)
    Console.WriteLine($"收到消息: {message}")
    wsClient.SendMessageAsync($"hi: {Date.Now:T}")
End Sub

Private Sub OnDisconnected(sender As Object, e As EventArgs)
    Console.WriteLine("连接已断开!")
End Sub

Private Sub OnError(sender As Object, e As WebSocketErrorEventArgs)
    Console.WriteLine($"错误: {e.ErrorCode}, {e.ErrorMessage}")
End Sub

相关方法

Private wsClient As New WebSocketClient() '实例化WebSocket客户端
Private wsClient As New WebSocketClient("wss://echo.websocket.org/") '实例化WebSocket服务端,支持ws和wss,默认5秒超时
Private wsClient As New WebSocketClient("wss://echo.websocket.org/",10) '自定义超时时间10秒

wsClient.StartConnect() '连接服务端
wsClient.StopConnect()  '断开连接
wsClient.SendMessageAsync(message) '发送消息
wsClient.SendPingAsync() '发送ping消息,默认“ping”
wsClient.SendPingAsync("hi") '发送自定义ping消息

12.二次开发

PicoServer 开放了 中间件 ,可以借此进行二次开发、封装、集成解决方案等 。

利用 AddMiddleware() 可开发属于自己的中间件。比如:参数路由,限流,黑名单等等。

PicoServer 中间件采用责任链模式,按添加顺序执行。

二次开发示例参考:🛠️二次开发

发布时间: