Merge remote-tracking branch 'upstream/v2' into v2

# Conflicts:
#	Admin.NET/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs
This commit is contained in:
suncaomei 2025-02-24 12:46:09 +08:00
commit 894baed53f
34 changed files with 1356 additions and 276 deletions

View File

@ -1,10 +1,13 @@
{
{
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
// MQTT
"Mqtt": {
"Enabled": false, //
"Port": "5001",
"IPAddress": ""
"Port": "1883", //
"IPAddress": "", // IP
"ConnectionBacklog": 1000, //
"MqttServerId": "Admin.NET.MQTT", // ClientId
"Logging": false //
}
}

View File

@ -14,20 +14,20 @@
<ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="3.1.1" />
<PackageReference Include="AlipaySDKNet.Standard" Version="4.9.409" />
<PackageReference Include="AlipaySDKNet.Standard" Version="4.9.412" />
<PackageReference Include="AngleSharp" Version="1.2.0" />
<PackageReference Include="AspectCore.Extensions.Reflection" Version="2.4.0" />
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" Aliases="BouncyCastleV2" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.17.1" />
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.8" />
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.8" />
<PackageReference Include="Furion.Pure" Version="4.9.7.8" />
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.10" />
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.10" />
<PackageReference Include="Furion.Pure" Version="4.9.7.10" />
<PackageReference Include="Hardware.Info" Version="101.0.1" />
<PackageReference Include="Hashids.net" Version="1.7.0" />
<PackageReference Include="IPTools.China" Version="1.6.0" />
<PackageReference Include="IPTools.International" Version="1.6.0" />
<PackageReference Include="log4net" Version="3.0.3" />
<PackageReference Include="log4net" Version="3.0.4" />
<PackageReference Include="Magicodes.IE.Excel" Version="2.7.5.2" />
<PackageReference Include="Magicodes.IE.Pdf" Version="2.7.5.2" />
<PackageReference Include="Magicodes.IE.Word" Version="2.7.5.2" />
@ -40,7 +40,7 @@
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
<PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.2.0" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.1.0" />
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.7.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.10.0" />
@ -48,7 +48,7 @@
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.0.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1182" />
<PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1184" />
<PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup>

View File

@ -0,0 +1,239 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
using Microsoft.Extensions.Hosting;
using MQTTnet;
using MQTTnet.Protocol;
using MQTTnet.Server;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// MQTT 服务
/// </summary>
public class MqttHostedService(IOptions<MqttOptions> mqttOptions) : IHostedService, ISingleton
{
private readonly MqttOptions _mqttOptions = mqttOptions.Value;
public static MqttServer MqttServer { get; set; }
public static readonly List<MqttEventInterceptor> MqttEventInterceptors = []; // MQTT 事件拦截器集合
/// <summary>
/// 注册 MQTT 事件拦截器
/// </summary>
/// <param name="mqttEventInterceptor"></param>
/// <param name="order"></param>
public static void AddMqttEventInterceptor(MqttEventInterceptor mqttEventInterceptor, int order = 0)
{
mqttEventInterceptor.Order = order;
MqttEventInterceptors.Add(mqttEventInterceptor);
MqttEventInterceptors.Sort((a, b) => b.Order - a.Order);
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_mqttOptions.Enabled) return;
// 注册 MQTT 自定义客户端验证事件拦截器
AddMqttEventInterceptor(new DefaultMqttEventInterceptor());
var options = new MqttServerOptionsBuilder()
.WithDefaultEndpoint() // 默认地址127.0.0.1
.WithDefaultEndpointPort(_mqttOptions.Port) // 端口号
//.WithDefaultEndpointBoundIPAddress(_mqttOptions.IPAddress) // IP地址
.WithConnectionBacklog(_mqttOptions.ConnectionBacklog) // 最大连接数
.WithPersistentSessions()
.Build();
MqttServer = new MqttServerFactory().CreateMqttServer(options);
MqttServer.StartedAsync += MqttServer_StartedAsync; // 启动后事件
MqttServer.StoppedAsync += MqttServer_StoppedAsync; // 关闭后事件
MqttServer.ValidatingConnectionAsync += MqttServer_ValidatingConnectionAsync; // 客户端验证事件
MqttServer.ClientConnectedAsync += MqttServer_ClientConnectedAsync; // 客户端连接事件
MqttServer.ClientDisconnectedAsync += MqttServer_ClientDisconnectedAsync; // 客户端断开事件
MqttServer.ClientSubscribedTopicAsync += MqttServer_ClientSubscribedTopicAsync; // 订阅主题事件
MqttServer.ClientUnsubscribedTopicAsync += MqttServer_ClientUnsubscribedTopicAsync; // 取消订阅事件
MqttServer.InterceptingPublishAsync += MqttServer_InterceptingPublishAsync; // 拦截接收消息
MqttServer.ApplicationMessageNotConsumedAsync += MqttServer_ApplicationMessageNotConsumedAsync; // 消息未被消费
await MqttServer.StartAsync();
}
/// <summary>
/// 启动后事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_StartedAsync(EventArgs arg)
{
Console.WriteLine($"【MQTT】服务已启动端口{_mqttOptions.Port}...... {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 关闭后事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_StoppedAsync(EventArgs arg)
{
Console.WriteLine($"【MQTT】服务已关闭...... {DateTime.Now}");
foreach (var eh in MqttEventInterceptors)
{
await eh.StoppedAsync(arg);
}
}
/// <summary>
/// 客户端验证事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_ValidatingConnectionAsync(ValidatingConnectionEventArgs arg)
{
foreach (var eh in MqttEventInterceptors)
{
await eh.ValidatingConnectionAsync(arg);
if (arg.ReasonCode != MqttConnectReasonCode.Success)
break;
}
}
/// <summary>
/// 客户端连接事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_ClientConnectedAsync(ClientConnectedEventArgs arg)
{
foreach (var eh in MqttEventInterceptors)
{
await eh.ClientConnectedAsync(arg);
}
Logging($"客户端连接客户端ID=【{arg.ClientId}】已连接:用户名=【{arg.UserName}】地址=【{arg.RemoteEndPoint}】 {DateTime.Now}");
}
/// <summary>
/// 客户端断开事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private async Task MqttServer_ClientDisconnectedAsync(ClientDisconnectedEventArgs arg)
{
foreach (var eh in MqttEventInterceptors)
{
await eh.ClientDisconnectedAsync(arg);
}
Logging($"客户端断开客户端ID=【{arg.ClientId}】已断开:用户名=【{arg.UserName}】地址=【{arg.RemoteEndPoint}】 {DateTime.Now}");
}
/// <summary>
/// 订阅主题事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_ClientSubscribedTopicAsync(ClientSubscribedTopicEventArgs arg)
{
foreach (var eh in MqttEventInterceptors)
{
await eh.ClientSubscribedTopicAsync(arg);
}
Logging($"订阅主题客户端ID=【{arg.ClientId}】订阅主题=【{arg.TopicFilter}】 {DateTime.Now}");
}
/// <summary>
/// 取消订阅事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_ClientUnsubscribedTopicAsync(ClientUnsubscribedTopicEventArgs arg)
{
foreach (var eh in MqttEventInterceptors)
{
await eh.ClientUnsubscribedTopicAsync(arg);
}
Logging($"取消订阅客户端ID=【{arg.ClientId}】取消订阅主题=【{arg.TopicFilter}】 {DateTime.Now}");
}
/// <summary>
/// 拦截发布的消息事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_InterceptingPublishAsync(InterceptingPublishEventArgs arg)
{
if (string.Equals(arg.ClientId, _mqttOptions.MqttServerId))
return;
foreach (var eh in MqttEventInterceptors)
{
await eh.InterceptingPublishAsync(arg);
}
Logging($"拦截消息客户端ID=【{arg.ClientId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】 {DateTime.Now}");
}
/// <summary>
/// 未被消费的消息事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private async Task MqttServer_ApplicationMessageNotConsumedAsync(ApplicationMessageNotConsumedEventArgs arg)
{
foreach (var eh in MqttEventInterceptors)
{
await eh.ApplicationMessageNotConsumedAsync(arg);
}
Logging($"接收消息发送端ID=【{arg.SenderId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】 {DateTime.Now}");
}
/// <summary>
/// 发布主题消息
/// </summary>
/// <param name="topic"></param>
/// <param name="message"></param>
public async Task PublicMessageAsync(string topic, string message)
{
var applicationMessage = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(message)
.Build();
await MqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(applicationMessage)
{
SenderClientId = _mqttOptions.MqttServerId,
SenderUserName = _mqttOptions.MqttServerId,
});
Logging($"服务器发布主题:{topic}, 内容:{message}");
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// 输出日志
/// </summary>
/// <param name="msg"></param>
protected void Logging(string msg)
{
if (!_mqttOptions.Logging) return;
LoggingWriter.LogInformation(msg);
}
}

View File

@ -0,0 +1,47 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using MQTTnet.Protocol;
using MQTTnet.Server;
namespace Admin.NET.Core;
/// <summary>
/// 默认 MQTT 事件拦截器
/// </summary>
public class DefaultMqttEventInterceptor : MqttEventInterceptor
{
public new int Order = int.MinValue;
public override Task ValidatingConnectionAsync(ValidatingConnectionEventArgs arg)
{
var _db = App.GetRequiredService<ISqlSugarClient>();
// 验证账号
var user = _db.Queryable<SysUser>().First(u => u.Account == arg.UserName);
if (user == null)
{
arg.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
Logging($"客户端验证客户端ID=【{arg.ClientId}】用户名不存在 {DateTime.Now} ");
return Task.CompletedTask;
}
// 验证密码
var password = arg.Password;
if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
{
if (user.Password.Equals(MD5Encryption.Encrypt(password)))
return Task.CompletedTask;
}
else
{
if (CryptogramUtil.Decrypt(user.Password).Equals(password))
return Task.CompletedTask;
}
arg.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
Logging($"客户端验证客户端ID=【{arg.ClientId}】用户名或密码验证错误 {DateTime.Now} ");
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,120 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using MQTTnet.Server;
namespace Admin.NET.Core;
/// <summary>
/// MQTT 事件拦截器
/// </summary>
public class MqttEventInterceptor
{
/// <summary>
/// 数值越大越先执行
/// </summary>
public int Order = 0;
/// <summary>
/// 启动后事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task StartedAsync(EventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 关闭后事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task StoppedAsync(EventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 客户端验证事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task ValidatingConnectionAsync(ValidatingConnectionEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 客户端连接事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task ClientConnectedAsync(ClientConnectedEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 客户端断开事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task ClientDisconnectedAsync(ClientDisconnectedEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 订阅主题事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task ClientSubscribedTopicAsync(ClientSubscribedTopicEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 取消订阅事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task ClientUnsubscribedTopicAsync(ClientUnsubscribedTopicEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 拦截发布的消息事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task InterceptingPublishAsync(InterceptingPublishEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 未被消费的消息事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
public virtual async Task ApplicationMessageNotConsumedAsync(ApplicationMessageNotConsumedEventArgs arg)
{
await Task.CompletedTask;
}
/// <summary>
/// 输出日志事件
/// </summary>
/// <param name="msg"></param>
protected static void Logging(string msg)
{
if (!App.GetOptions<MqttOptions>().Logging) return;
LoggingWriter.LogInformation(msg);
}
}

View File

@ -1,189 +0,0 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
using Microsoft.Extensions.Hosting;
using MQTTnet.Protocol;
using MQTTnet.Server;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// MQTT 服务
/// </summary>
public class MqttHostedService(IOptions<MqttOptions> mqttOptions, ISqlSugarClient db) : IHostedService, IDisposable
{
private const string ServerClientId = "Admin.NET.MQTT";
public MqttServer MqttServer { get; set; }
private readonly MqttOptions _mqttOptions = mqttOptions.Value;
private readonly ISqlSugarClient _db = db;
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_mqttOptions.Enabled) return;
var options = new MqttServerOptionsBuilder()
.WithDefaultEndpoint() // 默认地址127.0.0.1
.WithDefaultEndpointPort(_mqttOptions.Port) // 端口号
//.WithDefaultEndpointBoundIPAddress(_mqttOptions.IPAddress) // IP地址
.WithConnectionBacklog(1000) // 最大连接数
.WithPersistentSessions()
.Build();
MqttServer = new MqttServerFactory().CreateMqttServer(options);
MqttServer.StartedAsync += MqttServer_StartedAsync; // 启动后事件
MqttServer.StoppedAsync += MqttServer_StoppedAsync; // 关闭后事件
MqttServer.ValidatingConnectionAsync += MqttServer_ValidatingConnectionAsync; // 客户端验证事件
MqttServer.ClientConnectedAsync += MqttServer_ClientConnectedAsync; // 客户端连接事件
MqttServer.ClientDisconnectedAsync += MqttServer_ClientDisconnectedAsync; // 客户端断开事件
MqttServer.ClientSubscribedTopicAsync += MqttServer_ClientSubscribedTopicAsync; // 订阅主题事件
MqttServer.ClientUnsubscribedTopicAsync += MqttServer_ClientUnsubscribedTopicAsync; // 取消订阅事件
MqttServer.InterceptingPublishAsync += MqttServer_InterceptingPublishAsync; // 拦截接收消息
MqttServer.ApplicationMessageNotConsumedAsync += MqttServer_ApplicationMessageNotConsumedAsync; // 消息未被消费
await MqttServer.StartAsync();
}
/// <summary>
/// 启动后事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_StartedAsync(EventArgs arg)
{
Console.WriteLine($"【MQTT】服务已启动端口{_mqttOptions.Port}...... {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 关闭后事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_StoppedAsync(EventArgs arg)
{
Console.WriteLine($"【MQTT】服务已关闭...... {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 客户端验证事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_ValidatingConnectionAsync(ValidatingConnectionEventArgs arg)
{
arg.ReasonCode = MqttConnectReasonCode.Success;
// 验证账号
var user = _db.Queryable<SysUser>().First(u => u.Account == arg.UserName);
if (user == null)
{
arg.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
Console.WriteLine($"客户端验证客户端ID=【{arg.ClientId}】用户名或密码验证错误 {DateTime.Now} ");
throw Oops.Oh("MQTT客户端验证账号不能为空或不存在");
}
// 验证密码
var password = arg.Password;
if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
{
if (user.Password.Equals(MD5Encryption.Encrypt(password))) return Task.CompletedTask;
}
else
{
if (CryptogramUtil.Decrypt(user.Password).Equals(password)) return Task.CompletedTask;
}
arg.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
Console.WriteLine($"客户端验证客户端ID=【{arg.ClientId}】用户名或密码验证错误 {DateTime.Now} ");
throw Oops.Oh("MQTT客户端验证密码错误");
}
/// <summary>
/// 客户端连接事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_ClientConnectedAsync(ClientConnectedEventArgs arg)
{
Console.WriteLine($"客户端连接客户端ID=【{arg.ClientId}】已连接:用户名=【{arg.UserName}】地址=【{arg.RemoteEndPoint}】 {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 客户端断开事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private Task MqttServer_ClientDisconnectedAsync(ClientDisconnectedEventArgs arg)
{
Console.WriteLine($"客户端断开客户端ID=【{arg.ClientId}】已断开:用户名=【{arg.UserName}】地址=【{arg.RemoteEndPoint}】 {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 订阅主题事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_ClientSubscribedTopicAsync(ClientSubscribedTopicEventArgs arg)
{
Console.WriteLine($"订阅主题客户端ID=【{arg.ClientId}】订阅主题=【{arg.TopicFilter}】 {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 取消订阅事件
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_ClientUnsubscribedTopicAsync(ClientUnsubscribedTopicEventArgs arg)
{
Console.WriteLine($"取消订阅客户端ID=【{arg.ClientId}】取消订阅主题=【{arg.TopicFilter}】 {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 拦截接收消息
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_InterceptingPublishAsync(InterceptingPublishEventArgs arg)
{
if (string.Equals(arg.ClientId, ServerClientId))
return Task.CompletedTask;
Console.WriteLine($"拦截消息客户端ID=【{arg.ClientId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】 {DateTime.Now}");
return Task.CompletedTask;
}
/// <summary>
/// 消息未被消费
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private Task MqttServer_ApplicationMessageNotConsumedAsync(ApplicationMessageNotConsumedEventArgs arg)
{
Console.WriteLine($"接收消息发送端ID=【{arg.SenderId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】 {DateTime.Now}");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Dispose()
{
throw new NotImplementedException();
}
}

View File

@ -1,28 +1,43 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// MQTT 配置选项
/// </summary>
public sealed class MqttOptions : IConfigurableOptions
{
/// <summary>
/// 是否启用
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string IPAddress { get; set; }
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// MQTT 配置选项
/// </summary>
public sealed class MqttOptions : IConfigurableOptions
{
/// <summary>
/// 是否启用
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string IPAddress { get; set; }
/// <summary>
/// 最大连接数
/// </summary>
public int ConnectionBacklog { get; set; }
/// <summary>
/// 服务器主动发消息时的ClientId
/// </summary>
public string MqttServerId { get; set; }
/// <summary>
/// 输出日志
/// </summary>
public bool Logging { get; set; }
}

View File

@ -0,0 +1,23 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 发布主题消息
/// </summary>
public class PublicMessageInput
{
/// <summary>
/// 主题名称
/// </summary>
public string Topic { get; set; }
/// <summary>
/// 消息内容
/// </summary>
public string Message { get; set; }
}

View File

@ -0,0 +1,41 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using MQTTnet.Server;
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统 MQTT 服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 90, Description = "MQTT 服务")]
public class SysMqttService() : IDynamicApiController, ITransient
{
/// <summary>
/// 获取客户端列表 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取客户端列表")]
public async Task<IList<MqttClientStatus>> GetClients()
{
if (MqttHostedService.MqttServer == null)
throw Oops.Oh("【MQTT】服务未启动");
return await MqttHostedService.MqttServer.GetClientsAsync();
}
/// <summary>
/// 发布主题消息 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("发布主题消息")]
public async Task PublicMessage(PublicMessageInput input)
{
var mqttHostedService = App.GetRequiredService<MqttHostedService>();
await mqttHostedService.PublicMessageAsync(input.Topic, input.Message);
}
}

View File

@ -119,8 +119,7 @@ public class SysOnlineUserService : IDynamicApiController, ITransient
if (await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysSingleLogin)) return;
// 相同账号最后登录的用户Id集合
var onlineUserRecords = await _sysOnlineUerRep.AsQueryable()
.GroupBy(u => u.UserId)
var onlineUsers = await _sysOnlineUerRep.AsQueryable().GroupBy(u => u.UserId)
.Select(u => new
{
UserId = u.UserId,
@ -128,9 +127,10 @@ public class SysOnlineUserService : IDynamicApiController, ITransient
Id = SqlFunc.AggregateMax(u.Id)
})
.ToListAsync();
var onlineUserIds = onlineUserRecords.Select(x => x.Id).ToList();
if (onlineUsers.Count < 1) return;
// 无效登录用户集合
var onlineUserIds = onlineUsers.Select(u => u.Id).ToList();
var offlineUsers = await _sysOnlineUerRep.AsQueryable().Where(u => !onlineUserIds.Contains(u.Id)).ToListAsync();
foreach (var user in offlineUsers)
{

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="IGeekFan.AspNetCore.Knife4jUI" Version="0.0.16" />
<PackageReference Include="MQTTnet.AspNetCore" Version="5.0.1.1416" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="9.0.2" />
</ItemGroup>

View File

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using MQTTnet.AspNetCore;
using Newtonsoft.Json;
using OnceMi.AspNetCore.OSS;
using RabbitMQ.Client;
@ -106,7 +107,8 @@ public class Startup : AppStartup
// setting.MetadataPropertyHandling = MetadataPropertyHandling.Ignore; // 解决DateTimeOffset异常
// setting.DateParseHandling = DateParseHandling.None; // 解决DateTimeOffset异常
// setting.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }); // 解决DateTimeOffset异常
};
}
;
services.AddControllersWithViews()
.AddAppLocalization()
@ -410,14 +412,22 @@ public class Startup : AppStartup
}
});
var mqttOptions = App.GetConfig<MqttOptions>("Mqtt", true);
app.UseEndpoints(endpoints =>
{
// 注册集线器
endpoints.MapHubs();
// 注册路由
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// 注册 MQTT 支持 WebSocket
if (mqttOptions.Enabled)
{
endpoints.MapConnectionHandler<MqttConnectionHandler>("/mqtt",
httpConnectionDispatcherOptions => httpConnectionDispatcherOptions.WebSockets.SubProtocolSelector =
protocolList => protocolList.FirstOrDefault() ?? string.Empty);
}
});
}
}

View File

@ -165,7 +165,7 @@
@:</template>
} else if(@column.EffectType == "DictSelector") {
@:<template #row_@(@column.LowerPropertyName)="{ row }">
@:<el-tag v-if="row.@(@column.LowerPropertyName)" :type="dc('@(@column.DictTypeCode)', row.@(@column.LowerPropertyName))?.tagType"> {{dc('@(@column.DictTypeCode)', row.@column.LowerPropertyName)?.value}}</el-tag>
@:<el-tag v-if="row.@(@column.LowerPropertyName)" :type="dc('@(@column.DictTypeCode)', row.@(@column.LowerPropertyName))?.tagType"> {{dc('@(@column.DictTypeCode)', row.@column.LowerPropertyName)?.label}}</el-tag>
@:</template>
} else if(@column.EffectType == "EnumSelector") {
@:<template #row_@(@column.LowerPropertyName)="{ row }">
@ -258,8 +258,8 @@ const userStore = useUserInfo();
@:const codeToName = userStore.codeToName;
}
@if(@Model.TableField.Any(x=>x.EffectType == "DictSelector") || @Model.TableField.Any(x=>x.EffectType == "EnumSelector")){
@:const dc = userStore.getDictItemByCode;
@:const dv = userStore.getDictLabelByVal;
@:const dc = userStore.getDictItemByValue;
@:const dv = userStore.getDictItemByLabel;
@:const dl = userStore.getDictDataByCode;
}

View File

@ -224,8 +224,6 @@ const userStore = useUserInfo();
@:const getConstType = userStore.getConstDataByTypeCode;
}
@if(@Model.TableField.Any(x=>x.EffectType == "DictSelector") || @Model.TableField.Any(x=>x.EffectType == "EnumSelector")){
//@:const dc = userStore.getDictItemByCode;
//@:const dv = userStore.getDictLabelByVal;
@:const dl = userStore.getDictDataByCode;
}

View File

@ -158,7 +158,7 @@
else if(@column.EffectType == "DictSelector"){
@:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="" >
@:<template #default="scope">
@:<el-tag :type="dc('@(@column.DictTypeCode)', scope.row.@(@column.LowerPropertyName))?.tagType"> {{dc("@(@column.DictTypeCode)", scope.row.@(@column.LowerPropertyName))?.value}} </el-tag>
@:<el-tag :type="dc('@(@column.DictTypeCode)', scope.row.@(@column.LowerPropertyName))?.tagType"> {{dc("@(@column.DictTypeCode)", scope.row.@(@column.LowerPropertyName))?.label}} </el-tag>
@:</template>
@:</el-table-column>
}
@ -251,8 +251,8 @@
@:const codeToName = userStore.codeToName;
}
@if(@Model.TableField.Any(x=>x.EffectType == "DictSelector") || @Model.TableField.Any(x=>x.EffectType == "EnumSelector")){
@:const dc = userStore.getDictItemByCode;
@:const dv = userStore.getDictLabelByVal;
@:const dc = userStore.getDictItemByValue;
@:const dv = userStore.getDictItemByLabel;
@:const dl = userStore.getDictDataByCode;
}

View File

@ -2,7 +2,7 @@
"name": "admin.net.pro",
"type": "module",
"version": "2.4.33",
"lastBuildTime": "2025.02.18",
"lastBuildTime": "2025.02.23",
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
"author": "zuohuaijun",
"license": "MIT",
@ -18,8 +18,8 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@logicflow/core": "^2.0.10",
"@logicflow/extension": "^2.0.14",
"@logicflow/core": "^2.0.11",
"@logicflow/extension": "^2.0.15",
"@microsoft/signalr": "^8.0.7",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.14",
@ -36,7 +36,7 @@
"echarts": "^5.6.0",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.9.4",
"element-plus": "^2.9.5",
"exceljs": "^4.4.0",
"ezuikit-js": "^8.1.6",
"gcoord": "^1.0.7",
@ -45,7 +45,7 @@
"jsplumb": "^2.15.6",
"jwchat": "^2.0.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^5.2.3",
"md-editor-v3": "^5.3.2",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"mqtt": "^5.10.3",
@ -74,7 +74,7 @@
"vue-router": "^4.5.0",
"vue-signature-pad": "^3.0.2",
"vue3-tree-org": "^4.2.2",
"vxe-pc-ui": "^4.3.90",
"vxe-pc-ui": "^4.3.95",
"vxe-table": "^4.10.0",
"vxe-table-plugin-element": "^4.0.4",
"vxe-table-plugin-export-xlsx": "^4.0.7",
@ -94,16 +94,16 @@
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/compiler-sfc": "^3.5.13",
"code-inspector-plugin": "^0.20.0",
"eslint": "^9.20.1",
"eslint": "^9.21.0",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.15.0",
"globals": "^16.0.0",
"less": "^4.2.2",
"prettier": "^3.5.1",
"prettier": "^3.5.2",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.85.0",
"terser": "^5.39.0",
"typescript": "^5.7.3",
"vite": "^6.1.0",
"vite": "^6.1.1",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-vue-setup-extend": "^0.4.0",

View File

@ -38,6 +38,7 @@ export * from './apis/sys-log-op-api';
export * from './apis/sys-log-vis-api';
export * from './apis/sys-menu-api';
export * from './apis/sys-message-api';
export * from './apis/sys-mqtt-api';
export * from './apis/sys-notice-api';
export * from './apis/sys-oauth-api';
export * from './apis/sys-oauth-user-api';

View File

@ -77,6 +77,49 @@ export const SysEnumApiAxiosParamCreator = function (configuration?: Configurati
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysEnumEnumToDictPost: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysEnum/enumToDict`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
@ -143,6 +186,19 @@ export const SysEnumApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysEnumEnumToDictPost(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
const localVarAxiosArgs = await SysEnumApiAxiosParamCreator(configuration).apiSysEnumEnumToDictPost(options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
@ -175,6 +231,15 @@ export const SysEnumApiFactory = function (configuration?: Configuration, basePa
async apiSysEnumEnumDataListGet(enumName: string, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultListEnumEntity>> {
return SysEnumApiFp(configuration).apiSysEnumEnumDataListGet(enumName, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysEnumEnumToDictPost(options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
return SysEnumApiFp(configuration).apiSysEnumEnumToDictPost(options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
@ -205,6 +270,16 @@ export class SysEnumApi extends BaseAPI {
public async apiSysEnumEnumDataListGet(enumName: string, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultListEnumEntity>> {
return SysEnumApiFp(this.configuration).apiSysEnumEnumDataListGet(enumName, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysEnumApi
*/
public async apiSysEnumEnumToDictPost(options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
return SysEnumApiFp(this.configuration).apiSysEnumEnumToDictPost(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖

View File

@ -0,0 +1,214 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import globalAxios, { AxiosResponse, AxiosInstance, AxiosRequestConfig } from 'axios';
import { Configuration } from '../configuration';
// Some imports not used depending on template conditions
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
import { AdminNETResultIListMqttClientStatus } from '../models';
import { PublicMessageInput } from '../models';
/**
* SysMqttApi - axios parameter creator
* @export
*/
export const SysMqttApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysMqttClientsGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysMqtt/clients`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
* @param {PublicMessageInput} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysMqttPublicMessagePost: async (body?: PublicMessageInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysMqtt/publicMessage`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
}
};
/**
* SysMqttApi - functional programming interface
* @export
*/
export const SysMqttApiFp = function(configuration?: Configuration) {
return {
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysMqttClientsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultIListMqttClientStatus>>> {
const localVarAxiosArgs = await SysMqttApiAxiosParamCreator(configuration).apiSysMqttClientsGet(options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
* @param {PublicMessageInput} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysMqttPublicMessagePost(body?: PublicMessageInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
const localVarAxiosArgs = await SysMqttApiAxiosParamCreator(configuration).apiSysMqttPublicMessagePost(body, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
}
};
/**
* SysMqttApi - factory interface
* @export
*/
export const SysMqttApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
return {
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysMqttClientsGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultIListMqttClientStatus>> {
return SysMqttApiFp(configuration).apiSysMqttClientsGet(options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
* @param {PublicMessageInput} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysMqttPublicMessagePost(body?: PublicMessageInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
return SysMqttApiFp(configuration).apiSysMqttPublicMessagePost(body, options).then((request) => request(axios, basePath));
},
};
};
/**
* SysMqttApi - object-oriented interface
* @export
* @class SysMqttApi
* @extends {BaseAPI}
*/
export class SysMqttApi extends BaseAPI {
/**
*
* @summary 🔖
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysMqttApi
*/
public async apiSysMqttClientsGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultIListMqttClientStatus>> {
return SysMqttApiFp(this.configuration).apiSysMqttClientsGet(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖
* @param {PublicMessageInput} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysMqttApi
*/
public async apiSysMqttPublicMessagePost(body?: PublicMessageInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
return SysMqttApiFp(this.configuration).apiSysMqttPublicMessagePost(body, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -74,6 +74,49 @@ export const SysOnlineUserApiAxiosParamCreator = function (configuration?: Confi
options: localVarRequestOptions,
};
},
/**
*
* @summary 线
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysOnlineUserOnlinePost: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysOnlineUser/online`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary 线 🔖
@ -145,6 +188,19 @@ export const SysOnlineUserApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 线
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysOnlineUserOnlinePost(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
const localVarAxiosArgs = await SysOnlineUserApiAxiosParamCreator(configuration).apiSysOnlineUserOnlinePost(options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 线 🔖
@ -178,6 +234,15 @@ export const SysOnlineUserApiFactory = function (configuration?: Configuration,
async apiSysOnlineUserForceOfflinePost(body?: SysOnlineUser, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
return SysOnlineUserApiFp(configuration).apiSysOnlineUserForceOfflinePost(body, options).then((request) => request(axios, basePath));
},
/**
*
* @summary 线
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysOnlineUserOnlinePost(options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
return SysOnlineUserApiFp(configuration).apiSysOnlineUserOnlinePost(options).then((request) => request(axios, basePath));
},
/**
*
* @summary 线 🔖
@ -209,6 +274,16 @@ export class SysOnlineUserApi extends BaseAPI {
public async apiSysOnlineUserForceOfflinePost(body?: SysOnlineUser, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
return SysOnlineUserApiFp(this.configuration).apiSysOnlineUserForceOfflinePost(body, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 线
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysOnlineUserApi
*/
public async apiSysOnlineUserOnlinePost(options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
return SysOnlineUserApiFp(this.configuration).apiSysOnlineUserOnlinePost(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 线 🔖

View File

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
* @export
* @enum {string}
*/
export enum AddressFamily {
NUMBER_0 = 0,
NUMBER_1 = 1,
NUMBER_2 = 2,
NUMBER_3 = 3,
NUMBER_4 = 4,
NUMBER_5 = 5,
NUMBER_6 = 6,
NUMBER_7 = 7,
NUMBER_8 = 8,
NUMBER_9 = 9,
NUMBER_10 = 10,
NUMBER_11 = 11,
NUMBER_12 = 12,
NUMBER_13 = 13,
NUMBER_14 = 14,
NUMBER_15 = 15,
NUMBER_16 = 16,
NUMBER_17 = 17,
NUMBER_18 = 18,
NUMBER_19 = 19,
NUMBER_21 = 21,
NUMBER_22 = 22,
NUMBER_23 = 23,
NUMBER_24 = 24,
NUMBER_25 = 25,
NUMBER_26 = 26,
NUMBER_28 = 28,
NUMBER_29 = 29,
NUMBER_65536 = 65536,
NUMBER_65537 = 65537,
NUMBER_MINUS_1 = -1
}

View File

@ -0,0 +1,71 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import { MqttClientStatus } from './mqtt-client-status';
/**
*
*
* @export
* @interface AdminNETResultIListMqttClientStatus
*/
export interface AdminNETResultIListMqttClientStatus {
/**
*
*
* @type {number}
* @memberof AdminNETResultIListMqttClientStatus
*/
code?: number;
/**
* successwarningerror
*
* @type {string}
* @memberof AdminNETResultIListMqttClientStatus
*/
type?: string | null;
/**
*
*
* @type {string}
* @memberof AdminNETResultIListMqttClientStatus
*/
message?: string | null;
/**
*
*
* @type {Array<MqttClientStatus>}
* @memberof AdminNETResultIListMqttClientStatus
*/
result?: Array<MqttClientStatus> | null;
/**
*
*
* @type {any}
* @memberof AdminNETResultIListMqttClientStatus
*/
extras?: any | null;
/**
*
*
* @type {Date}
* @memberof AdminNETResultIListMqttClientStatus
*/
time?: Date;
}

View File

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import { AddressFamily } from './address-family';
/**
*
*
* @export
* @interface EndPoint
*/
export interface EndPoint {
/**
* @type {AddressFamily}
* @memberof EndPoint
*/
addressFamily?: AddressFamily;
}

View File

@ -21,6 +21,7 @@ export * from './add-sys-ldap-input';
export * from './add-tenant-input';
export * from './add-upgrade-input';
export * from './add-user-input';
export * from './address-family';
export * from './admin-netresult-boolean';
export * from './admin-netresult-captcha-output';
export * from './admin-netresult-create-pay-transaction-native-output';
@ -34,6 +35,7 @@ export * from './admin-netresult-grant-role-output';
export * from './admin-netresult-iaction-result';
export * from './admin-netresult-idisposable';
export * from './admin-netresult-ienumerable-entity-info';
export * from './admin-netresult-ilist-mqtt-client-status';
export * from './admin-netresult-int32';
export * from './admin-netresult-int64';
export * from './admin-netresult-jobject';
@ -210,6 +212,7 @@ export * from './delete-user-input';
export * from './dict-data-input';
export * from './dict-type-input';
export * from './digit-shapes';
export * from './end-point';
export * from './entity-column-info';
export * from './entity-info';
export * from './enum-entity';
@ -276,6 +279,9 @@ export * from './method-impl-attributes';
export * from './method-info';
export * from './module';
export * from './module-handle';
export * from './mqtt-client-status';
export * from './mqtt-protocol-version';
export * from './mqtt-session-status';
export * from './navigate';
export * from './notice-input';
export * from './notice-status-enum';
@ -322,6 +328,7 @@ export * from './print-type-enum';
export * from './promotion';
export * from './property-attributes';
export * from './property-info';
export * from './public-message-input';
export * from './query-region-input';
export * from './refund-request-input';
export * from './reset-pwd-user-input';

View File

@ -0,0 +1,115 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import { EndPoint } from './end-point';
import { MqttProtocolVersion } from './mqtt-protocol-version';
import { MqttSessionStatus } from './mqtt-session-status';
/**
*
*
* @export
* @interface MqttClientStatus
*/
export interface MqttClientStatus {
/**
* @type {number}
* @memberof MqttClientStatus
*/
bytesReceived?: number;
/**
* @type {number}
* @memberof MqttClientStatus
*/
bytesSent?: number;
/**
* @type {Date}
* @memberof MqttClientStatus
*/
connectedTimestamp?: Date;
/**
* @type {EndPoint}
* @memberof MqttClientStatus
*/
remoteEndPoint?: EndPoint;
/**
* @type {string}
* @memberof MqttClientStatus
*/
endpoint?: string | null;
/**
* @type {string}
* @memberof MqttClientStatus
*/
id?: string | null;
/**
* @type {Date}
* @memberof MqttClientStatus
*/
lastNonKeepAlivePacketReceivedTimestamp?: Date;
/**
* @type {Date}
* @memberof MqttClientStatus
*/
lastPacketReceivedTimestamp?: Date;
/**
* @type {Date}
* @memberof MqttClientStatus
*/
lastPacketSentTimestamp?: Date;
/**
* @type {MqttProtocolVersion}
* @memberof MqttClientStatus
*/
protocolVersion?: MqttProtocolVersion;
/**
* @type {number}
* @memberof MqttClientStatus
*/
receivedApplicationMessagesCount?: number;
/**
* @type {number}
* @memberof MqttClientStatus
*/
receivedPacketsCount?: number;
/**
* @type {number}
* @memberof MqttClientStatus
*/
sentApplicationMessagesCount?: number;
/**
* @type {number}
* @memberof MqttClientStatus
*/
sentPacketsCount?: number;
/**
* @type {MqttSessionStatus}
* @memberof MqttClientStatus
*/
session?: MqttSessionStatus;
}

View File

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
* @export
* @enum {string}
*/
export enum MqttProtocolVersion {
NUMBER_0 = 0,
NUMBER_3 = 3,
NUMBER_4 = 4,
NUMBER_5 = 5
}

View File

@ -0,0 +1,58 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
*
* @export
* @interface MqttSessionStatus
*/
export interface MqttSessionStatus {
/**
* @type {Date}
* @memberof MqttSessionStatus
*/
createdTimestamp?: Date;
/**
* @type {Date}
* @memberof MqttSessionStatus
*/
disconnectedTimestamp?: Date | null;
/**
* @type {number}
* @memberof MqttSessionStatus
*/
expiryInterval?: number;
/**
* @type {string}
* @memberof MqttSessionStatus
*/
id?: string | null;
/**
* @type {{ [key: string]: any; }}
* @memberof MqttSessionStatus
*/
items?: { [key: string]: any; } | null;
/**
* @type {number}
* @memberof MqttSessionStatus
*/
pendingApplicationMessagesCount?: number;
}

View File

@ -0,0 +1,38 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
*
* @export
* @interface PublicMessageInput
*/
export interface PublicMessageInput {
/**
*
*
* @type {string}
* @memberof PublicMessageInput
*/
topic?: string | null;
/**
*
*
* @type {string}
* @memberof PublicMessageInput
*/
message?: string | null;
}

View File

@ -140,6 +140,14 @@ export interface SysFile {
*/
suffix?: string | null;
/**
* MIME类型
*
* @type {string}
* @memberof SysFile
*/
contentType?: string | null;
/**
*
*

View File

@ -139,18 +139,23 @@ export const useUserInfo = defineStore('userInfo', {
return data.find((item: any) => item.code === itemCode)?.name;
},
// 常量编码和名称转换
codeToName(code: any, type: any) {
return this.constList.find((x: any) => x.code === type).data.result.find((x: any) => x.code === code)?.name;
},
// 根据字典类型获取字典数据
getDictDataByCode(dictTypeCode: string) {
return this.dictList[dictTypeCode] || [];
},
// 根据字典类型和代码取字典项
getDictItemByCode(typePCode: string, code: string) {
if (code != undefined && code !== '') {
const _code = code.toString();
const ds = this.getDictDataByCode(typePCode);
// 根据字典类型和值获取取字典项
getDictItemByValue(dictTypeCode: string, value: string) {
if (value != undefined && value !== '') {
const _value = value.toString();
const ds = this.dictList[dictTypeCode] || [];
for (const element of ds) {
if (element.code == _code) {
if (element.value == _value) {
return element;
}
}
@ -158,30 +163,25 @@ export const useUserInfo = defineStore('userInfo', {
return {};
},
// 根据字典类型和值取描述
getDictLabelByVal(typePCode: string, val: string) {
if (val != undefined && val !== '') {
const _val = val.toString();
const ds = this.getDictDataByCode(typePCode);
// 根据字典类型和名称获取取字典项
getDictItemByLabel(dictTypeCode: string, label: string) {
if (label != undefined && label !== '') {
const _label = label.toString();
const ds = this.dictList[dictTypeCode] || [];
for (const element of ds) {
if (element.value == _val) {
if (element.label == _label) {
return element;
}
}
}
return {};
},
// 常量编码和名称转换
codeToName(code: any, type: any) {
return this.constList.find((x: any) => x.code === type).data.result.find((x: any) => x.code === code)?.name;
},
},
});
// 处理字典国际化, 默认显示字典中的value
// 处理字典国际化, 默认显示字典中的label值
const setDictLangMessageAsync = async (dict: any) => {
dict.langMessage = `message.system.dictType.${dict.typeCode}.${dict.code}`;
const value = t(dict.langMessage);
dict.value = value !== dict.langMessage ? value : dict.value;
dict.langMessage = `message.dictType.${dict.typeCode}_${dict.value}`;
const text = t(dict.langMessage);
dict.label = text !== dict.langMessage ? text : dict.label;
};

View File

@ -10,7 +10,7 @@
</template>
<template #effectType="{ row, $index }">
<vxe-select v-model="row.effectType" class="m-2" style="width: 70%" placeholder="Select" transfer :disabled="judgeColumns(row)" @change="effectTypeChange(row, $index)" filterable>
<vxe-option v-for="item in state.effectTypeList" :key="item.code" :label="item.value" :value="item.code" />
<vxe-option v-for="item in state.effectTypeList" :key="item.value" :label="item.label" :value="item.value" />
</vxe-select>
<vxe-button v-if="row.effectType === 'ApiTreeSelector' || row.effectType === 'ForeignKey'" style="width: 30%" icon="vxe-icon-edit" @click="effectTypeChange(row, $index)">修改</vxe-button>
</template>
@ -43,7 +43,7 @@
</template>
<template #queryType="{ row }">
<vxe-select v-model="row.queryType" class="m-2" placeholder="Select" :disabled="!row.queryWhether" filterable transfer>
<vxe-option v-for="item in state.queryTypeList" :key="item.code" :label="item.value" :value="item.code" />
<vxe-option v-for="item in state.queryTypeList" :key="item.value" :label="item.label" :value="item.value" />
</vxe-select>
</template>
<template #verification="{ row }">

View File

@ -7,8 +7,8 @@ export const JobScriptCode = `// Admin.NET 项目的版权、商标、专利和
#region using
using Furion;
using Furion.HttpRemote;
using Furion.Logging;
using Furion.RemoteRequest.Extensions;
using Furion.Schedule;
using Microsoft.Extensions.DependencyInjection;
using System;
@ -45,7 +45,9 @@ public class DynamicJob : IJob
// var rep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysUser>>();
// 请求网址
// var result = await "http://www.baidu.com".GetAsStringAsync();
// var url = "http://www.baidu.com";
// var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
// var result = await httpRemoteService.GetAsStringAsync(url);
// Console.WriteLine(result);
// 日志

View File

@ -37,7 +37,7 @@
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="机构类型">
<el-select v-model="state.ruleForm.type" filterable clearable class="w100">
<el-option v-for="item in state.orgTypeList" :key="item.value" :label="item.value" :value="item.code" />
<el-option v-for="item in state.orgTypeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>

View File

@ -21,7 +21,7 @@
<el-col class="mb5" :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="机构类型" prop="type">
<el-select v-model="state.queryParams.type" filterable clearable class="w100" @clear="state.queryParams.type = undefined">
<el-option v-for="item in state.orgTypeList" :key="item.value" :label="item.value" :value="item.code" />
<el-option v-for="item in state.orgTypeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>