diff --git a/WeiCloud.Fusion/Alarm.Application/Alarm.Application.csproj b/WeiCloud.Fusion/AlarmService/Alarm.Application/Alarm.Application.csproj similarity index 100% rename from WeiCloud.Fusion/Alarm.Application/Alarm.Application.csproj rename to WeiCloud.Fusion/AlarmService/Alarm.Application/Alarm.Application.csproj diff --git a/WeiCloud.Fusion/Alarm.Application/RequestDto/SubscribeEventReqDto.cs b/WeiCloud.Fusion/AlarmService/Alarm.Application/RequestDto/SubscribeEventReqDto.cs similarity index 99% rename from WeiCloud.Fusion/Alarm.Application/RequestDto/SubscribeEventReqDto.cs rename to WeiCloud.Fusion/AlarmService/Alarm.Application/RequestDto/SubscribeEventReqDto.cs index caf5969..21a454e 100644 --- a/WeiCloud.Fusion/Alarm.Application/RequestDto/SubscribeEventReqDto.cs +++ b/WeiCloud.Fusion/AlarmService/Alarm.Application/RequestDto/SubscribeEventReqDto.cs @@ -7,7 +7,6 @@ namespace Alarm.Application.RequestDto [JsonPropertyName("param")] public required SubscribeParam Param { get; init; } } - public sealed class SubscribeParam { [JsonPropertyName("monitors")] diff --git a/WeiCloud.Fusion/Alarm.Application/ResponeDto/EventEnvelopeDto.cs b/WeiCloud.Fusion/AlarmService/Alarm.Application/ResponeDto/EventEnvelopeDto.cs similarity index 100% rename from WeiCloud.Fusion/Alarm.Application/ResponeDto/EventEnvelopeDto.cs rename to WeiCloud.Fusion/AlarmService/Alarm.Application/ResponeDto/EventEnvelopeDto.cs diff --git a/WeiCloud.Fusion/Alarm.DomainService/Alarm.DomainService.csproj b/WeiCloud.Fusion/AlarmService/Alarm.DomainService/Alarm.DomainService.csproj similarity index 64% rename from WeiCloud.Fusion/Alarm.DomainService/Alarm.DomainService.csproj rename to WeiCloud.Fusion/AlarmService/Alarm.DomainService/Alarm.DomainService.csproj index 1df926c..7adf5ab 100644 --- a/WeiCloud.Fusion/Alarm.DomainService/Alarm.DomainService.csproj +++ b/WeiCloud.Fusion/AlarmService/Alarm.DomainService/Alarm.DomainService.csproj @@ -13,9 +13,10 @@ + + + - - diff --git a/WeiCloud.Fusion/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs b/WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs similarity index 86% rename from WeiCloud.Fusion/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs rename to WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs index c95d75b..3f893c4 100644 --- a/WeiCloud.Fusion/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs +++ b/WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs @@ -1,10 +1,12 @@ using Alarm.Application.RequestDto; using Alarm.Application.ResponeDto; using Common.Shared.Application.DaHua; +using Common.Shared.DomainService; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; +using System; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text.Json; @@ -17,12 +19,37 @@ namespace Alarm.DomainService.DahAlarm private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly HttpClient _http; + private readonly MQTTClient _mqttClient; + private readonly IMqttClientService _mqttClientService; + private string mqttHostIp; + private int mqttHostPort; + private int mqttTimeout; + private string mqttUsername; + private string mqttPassword; + private string mqttClientId; + private string topicName; - public DahuaGeneralCtlService(ILogger logger, IConfiguration configuration, HttpClient http) + /// + /// + /// + /// + /// + /// + /// + public DahuaGeneralCtlService(ILogger logger, IConfiguration configuration, HttpClient http, MQTTClient mQTTClient, IMqttClientService mqttClientService) { _logger = logger; _configuration = configuration; _http = http; + _mqttClient = mQTTClient; + mqttHostIp = _configuration["SubscribeMQTT:HostIP"]!;//IP地址 + mqttHostPort = _configuration["SubscribeMQTT:HostPort"].ToInt();//端口号 + mqttTimeout = _configuration["SubscribeMQTT:Timeout"].ToInt();//超时时间 + mqttUsername = _configuration["SubscribeMQTT:UserName"]!;//用户名 + mqttPassword = _configuration["SubscribeMQTT:Password"]!;//密码 + mqttClientId = _configuration["SubscribeMQTT:ClientId"]!; + topicName = _configuration["SubscribeMQTT:TopicName"]!; + _mqttClientService = mqttClientService; } /// @@ -208,7 +235,7 @@ namespace Alarm.DomainService.DahAlarm }; } - if (!(body.Success && string.Equals(body.Code, "0", StringComparison.OrdinalIgnoreCase))) + if (!(body.Success && string.Equals((string?)body.Code, "0", StringComparison.OrdinalIgnoreCase))) { return new DaHApiResult { @@ -258,6 +285,12 @@ namespace Alarm.DomainService.DahAlarm //这是大华的残卫报警类型 if (dto.Info.AlarmType == 4321) { + //拼接物联平台标准的mqtt消息格式 + var payload = "[{\"taglabel\":\"" + dto.Info.DeviceCode + " + \".alart.\" + " + dto.Info.DeviceName + "\",\"value\":\"" + dto.Info.AlarmStat + "\",\"time\":\"" + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + "\"}]"; + + await _mqttClient.EnsureConnectedAsync(mqttHostIp, mqttHostPort, mqttUsername, mqttPassword, topicName, mqttClientId); + + await _mqttClientService.PublishAsync("/zrh/sun/alarm", payload); } } } diff --git a/WeiCloud.Fusion/Alarm.DomainService/DahAlarm/IDahuaGeneralCtlService.cs b/WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/IDahuaGeneralCtlService.cs similarity index 100% rename from WeiCloud.Fusion/Alarm.DomainService/DahAlarm/IDahuaGeneralCtlService.cs rename to WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/IDahuaGeneralCtlService.cs diff --git a/WeiCloud.Fusion/AlarmService/AlarmService.API/AlarmService.API.csproj b/WeiCloud.Fusion/AlarmService/AlarmService.API/AlarmService.API.csproj index 446c877..c867cbb 100644 --- a/WeiCloud.Fusion/AlarmService/AlarmService.API/AlarmService.API.csproj +++ b/WeiCloud.Fusion/AlarmService/AlarmService.API/AlarmService.API.csproj @@ -4,12 +4,15 @@ net8.0 enable enable + True - + + + diff --git a/WeiCloud.Fusion/AlarmService/AlarmService.API/Program.cs b/WeiCloud.Fusion/AlarmService/AlarmService.API/Program.cs index 5e4b3a3..357545a 100644 --- a/WeiCloud.Fusion/AlarmService/AlarmService.API/Program.cs +++ b/WeiCloud.Fusion/AlarmService/AlarmService.API/Program.cs @@ -1,6 +1,7 @@ using AlarmService.API.Infrastructure; using Autofac; using Autofac.Extensions.DependencyInjection; +using Common.Shared.DomainService; using Microsoft.OpenApi.Models; using NLog; using NLog.Extensions.Logging; @@ -42,6 +43,12 @@ namespace AlarmService.API #endregion Cors + #region עtoken + + builder.Services.AddSingleton(); + + #endregion עtoken + #region SwaggerUI builder.Services.AddEndpointsApiExplorer(); @@ -86,6 +93,14 @@ namespace AlarmService.API options.Limits.MaxRequestBodySize = 200 * 1024 * 1024; // Ĭ 200MB }); + #region mqttclient + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + #endregion mqttclient + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/WeiCloud.Fusion/AlarmService/AlarmService.API/appsettings.json b/WeiCloud.Fusion/AlarmService/AlarmService.API/appsettings.json index 10f68b8..355c787 100644 --- a/WeiCloud.Fusion/AlarmService/AlarmService.API/appsettings.json +++ b/WeiCloud.Fusion/AlarmService/AlarmService.API/appsettings.json @@ -5,5 +5,25 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "SubscribeMQTT": { + "TopicName": "datasource_points_AXYJPT_v4", + "ProjectId": 530522108656160, + "HostIP": "v4.weienergy.cn", + "HostPort": 18883, + "Timeout": 5000, + "UserName": "test", + "Password": "test123", + "ClientId": "datasource_points_AXYJPT_v4", + "ApiUrl": "http://v4.weienergy.cn/datastream" + }, + "AllowedHosts": "*", + //大华摄像头的配置 + "DahuaAuth": { + "Host": "demo.weienergy.cn:15214", + "ClientId": "taiyanggong", + + "ClientSecret": "6d6c78f8-3d4c-4e76-ab6b-827942a7b725", + "Username": "system", + "Password": "Admin123" + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/AppHost.cs b/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/AppHost.cs index 7128e49..d3e2148 100644 --- a/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/AppHost.cs +++ b/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/AppHost.cs @@ -6,9 +6,12 @@ var apiService = builder.AddProject("apiserv var videoapi = builder.AddProject("videoapi"); var alarmapi = builder.AddProject("alarmapi"); +var sharedapi = builder.AddProject("sharedapi"); + builder.AddProject("webfrontend") .WithReference(videoapi) .WithReference(alarmapi) + .WithReference(sharedapi) .WithExternalHttpEndpoints() .WithHttpHealthCheck("/health") .WithReference(apiService) diff --git a/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/Manage.AppHost.AppHost.csproj b/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/Manage.AppHost.AppHost.csproj index ed94359..716dcc0 100644 --- a/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/Manage.AppHost.AppHost.csproj +++ b/WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/Manage.AppHost.AppHost.csproj @@ -12,6 +12,7 @@ + diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.csproj b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.csproj new file mode 100644 index 0000000..844cff7 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + True + + + + + + + + + + + + + + + + + + + + + + diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.http b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.http new file mode 100644 index 0000000..24bd5d3 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.http @@ -0,0 +1,6 @@ +@Common.Shared.API_HostAddress = http://localhost:5258 + +GET {{Common.Shared.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/AutoMapperProfile.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/AutoMapperProfile.cs new file mode 100644 index 0000000..075247f --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/AutoMapperProfile.cs @@ -0,0 +1,15 @@ +namespace Video.API.Infrastructure +{ + /// + /// AutoMapper + /// + public class AutoMapperProfile + { + /// + /// 캯 + /// + public AutoMapperProfile() + { + } + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/ConfigureAutofac.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/ConfigureAutofac.cs new file mode 100644 index 0000000..e027a5e --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/ConfigureAutofac.cs @@ -0,0 +1,31 @@ +using Autofac; +using System.Reflection; + +namespace Common.Shared.API.Infrastructure +{ + /// + /// autofac + /// + public class ConfigureAutofac : Autofac.Module + { + /// + /// + /// + /// + protected override void Load(ContainerBuilder builder) + { + //Assembly assemblysServices1 = Assembly.Load("WeiCloud.Core"); + //builder.RegisterAssemblyTypes(assemblysServices1).Where(t => t.Namespace != "" && t.Namespace != null && t.Name.EndsWith("Service") && t.Namespace.StartsWith("WeiCloud.Core")) + // .AsImplementedInterfaces() + // .InstancePerLifetimeScope().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies); + + var assemblysServices = Assembly.Load("Common.Shared.DomainService"); + builder.RegisterAssemblyTypes(assemblysServices) + .Where(x => x.Name.EndsWith("Service")) + .AsImplementedInterfaces() + .InstancePerLifetimeScope().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies); + + // builder.RegisterType(typeof(GrainFactory)).PropertiesAutowired().InstancePerLifetimeScope(); + } + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/NLog.config b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/NLog.config new file mode 100644 index 0000000..55c5da9 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/NLog.config @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Program.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Program.cs new file mode 100644 index 0000000..c2abe8d --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Program.cs @@ -0,0 +1,123 @@ +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Common.Shared.API.Infrastructure; +using Microsoft.OpenApi.Models; +using NLog; +using NLog.Extensions.Logging; +using NLog.Web; +using System.Reflection; + +namespace Common.Shared.API +{ + public class Program + { + public static void Main(string[] args) + { + var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); + logger.Debug("init main"); + try + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddHttpContextAccessor(); + builder.Services.AddHttpClient(); + + builder.Services.AddControllers(); + builder.Services.AddSingleton(builder.Configuration); + + #region Cors + + builder.Services.AddCors(options => + { + options.AddPolicy("_myAllowSpecificOrigins", + builder => + { + builder.AllowAnyOrigin() //ԴʱAPIã + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() + .SetIsOriginAllowed((h) => true);//ΪSignalrӵ + }); + }); + + #endregion Cors + + #region SwaggerUI + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1.0", new OpenApiInfo + { + Version = "v1.0", + Title = "WeiCloud.IoT",// + Description = "һ廯ƽ̨", + Contact = new OpenApiContact + { + Name = "hi7t", + Email = "", + Url = null + } + }); + //c.OperationFilter(); + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath, true); + c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); + }); + + #endregion SwaggerUI + + builder.Services.AddLogging(m => { m.AddNLog(); }); + + #region Autofac + + builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer(builder => + { + builder.RegisterModule(new ConfigureAutofac()); + }); + + #endregion Autofac + + // ȫĬС + builder.WebHost.ConfigureKestrel(options => + { + options.Limits.MaxRequestBodySize = 200 * 1024 * 1024; // Ĭ 200MB + }); + + var app = builder.Build(); + + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "WeiCloud.IoT-v1.0"); + }); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapControllers(); + // Startup ʵ + var startup = new Startup(builder.Configuration); + startup.Configure(app, app.Environment, builder.Configuration); + app.Run(); + } + catch (Exception exception) + { + // NLog: catch setup errors + logger.Error(exception, "Stopped program because of exception"); + throw; + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + NLog.LogManager.Shutdown(); + } + } + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Properties/launchSettings.json b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Properties/launchSettings.json new file mode 100644 index 0000000..85ae56e --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25714", + "sslPort": 44346 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "http://localhost:5258", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:7150;http://localhost:5258", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Startup.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Startup.cs new file mode 100644 index 0000000..1b32cec --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Startup.cs @@ -0,0 +1,19 @@ +using WeiCloud.Utils.Common; + +namespace Common.Shared.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfiguration configuration) + { + ServiceLocator.Instance = app.ApplicationServices; + } + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/WeatherForecast.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/WeatherForecast.cs new file mode 100644 index 0000000..2af2eee --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace Common.Shared.API +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/appsettings.json b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/appsettings.json new file mode 100644 index 0000000..cc0cc39 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.API/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + //大华摄像头的配置 + "DahuaAuth": { + "Host": "demo.weienergy.cn:15214", + "ClientId": "taiyanggong", + "ClientSecret": "6d6c78f8-3d4c-4e76-ab6b-827942a7b725", + "Username": "system", + "Password": "Admin123" + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/DaHApiResult.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/DaHApiResult.cs index 43ed80f..0dff463 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/DaHApiResult.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/DaHApiResult.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Globalization; namespace Common.Shared.Application.DaHua { @@ -9,10 +11,73 @@ namespace Common.Shared.Application.DaHua public class DaHApiResult { [JsonPropertyName("code")] - public string Code { get; set; } + [JsonConverter(typeof(FlexibleStringConverter))] + public string? Code { get; set; } + // errMsg 和 desc 都可能出现,做一个统一的“Message”来使用 [JsonPropertyName("errMsg")] - public string Msg { get; set; } + public string? Msg { get; set; } + + [JsonPropertyName("desc")] + public string? Desc { get; set; } + + [JsonIgnore] + public string? Message => !string.IsNullOrWhiteSpace(Msg) ? Msg : Desc; + + [JsonPropertyName("data")] + public T? Data { get; set; } + + [JsonPropertyName("success")] + public bool Success { get; set; } + } + + public sealed class FlexibleStringConverter : JsonConverter + { + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + return reader.GetString(); + + case JsonTokenType.Number: + // 先尽量按整数,再按 decimal,最后兜底 double + if (reader.TryGetInt64(out long i)) + return i.ToString(CultureInfo.InvariantCulture); + if (reader.TryGetDecimal(out decimal m)) + return m.ToString(CultureInfo.InvariantCulture); + + double d = reader.GetDouble(); + return d.ToString(CultureInfo.InvariantCulture); + + case JsonTokenType.True: + return "true"; + + case JsonTokenType.False: + return "false"; + + case JsonTokenType.Null: + return null; + + default: + throw new JsonException($"Unsupported token for string: {reader.TokenType}"); + } + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value is null) { writer.WriteNullValue(); return; } + writer.WriteStringValue(value); + } + } + + public class DaHApiDescResult + { + [JsonPropertyName("code")] + public int Code { get; set; } // 修改为 int 类型 + + [JsonPropertyName("desc")] // 修改为 "desc" 而不是 "errMsg" + public string Desc { get; set; } [JsonPropertyName("data")] public T Data { get; set; } diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaVideoQueryDto.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaTokenQueryDto.cs similarity index 66% rename from WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaVideoQueryDto.cs rename to WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaTokenQueryDto.cs index b372dd6..53d3676 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaVideoQueryDto.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaTokenQueryDto.cs @@ -59,4 +59,34 @@ namespace Common.Shared.Application.DaHua [JsonPropertyName("verifyCodeFlag")] public int VerifyCodeFlag { get; set; } = 0; } + + /// + /// 刷新 access_token 的请求参数模型 + /// + public class RefreshTokenReqDto + { + /// + /// 认证类型,固定值:refresh_token + /// + [JsonPropertyName("grant_type")] + public string GrantType { get; set; } = "refresh_token"; // 默认值,通常固定 + + /// + /// 客户端ID(与认证接口中一致) + /// + [JsonPropertyName("client_id")] + public string ClientId { get; set; } + + /// + /// 客户端密钥(与认证接口中一致) + /// + [JsonPropertyName("client_secret")] + public string ClientSecret { get; set; } + + /// + /// 刷新令牌(refresh_token),用于获取新的 access_token + /// + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + } } \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaVideoResDto.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaTokenResDto.cs similarity index 88% rename from WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaVideoResDto.cs rename to WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaTokenResDto.cs index 594fd95..b2bc4ea 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaVideoResDto.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaTokenResDto.cs @@ -35,13 +35,13 @@ namespace Common.Shared.Application.DaHua public string ClientId { get; set; } /// - /// 授权范围,固定为 ["*"] + /// 授权范围 /// [JsonPropertyName("scope")] - public string[] Scope { get; set; } + public string Scope { get; set; } // 改为string类型 /// - /// access_token 有效期(秒),默认 2 小时(7200 秒) + /// access_token 有效期(秒) /// [JsonPropertyName("expires_in")] public long ExpiresIn { get; set; } @@ -50,19 +50,25 @@ namespace Common.Shared.Application.DaHua /// 鉴权 Token /// [JsonPropertyName("access_token")] - public string AccessToken { get; set; } + public string? AccessToken { get; set; } /// - /// 刷新 Token(有效期 1 天) + /// 刷新 Token /// [JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } /// - /// Token 类型,固定为 "bearer" + /// Token 类型 /// [JsonPropertyName("token_type")] public string TokenType { get; set; } = "bearer"; + + /// + /// 剩余天数(新增字段) + /// + [JsonPropertyName("remainderDays")] + public int RemainderDays { get; set; } } /// diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/Common.Shared.DomainService.csproj b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/Common.Shared.DomainService.csproj index 3530a2b..bad3a67 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/Common.Shared.DomainService.csproj +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/Common.Shared.DomainService.csproj @@ -9,7 +9,6 @@ - diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/ITokenProviderService.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/ITokenProviderService.cs new file mode 100644 index 0000000..3540a21 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/ITokenProviderService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Common.Shared.DomainService +{ + public interface ITokenProviderService + { + Task GetTokenAsync(string clientId); + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenCacheService.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenCacheService.cs new file mode 100644 index 0000000..1725135 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenCacheService.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; + +namespace Common.Shared.DomainService +{ + public static class TokenCache + { + public static readonly ConcurrentDictionary TokenMap = new(); + } + + public static class TokenLockProvider + { + private static readonly ConcurrentDictionary LockMap = new(); + + public static SemaphoreSlim GetLock(string key) + { + return LockMap.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + } + } + + /// + /// 用于token缓存的条目类 + /// + public class TokenEntry + { + /// + /// token:这是token_type + 空格 + access_token这样格式的 + /// + public string? AccessToken { get; set; } + + /// + /// 添加时间 + /// + public DateTimeOffset ExpireAt { get; set; } + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenProviderService.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenProviderService.cs new file mode 100644 index 0000000..7c75d56 --- /dev/null +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenProviderService.cs @@ -0,0 +1,349 @@ +using Common.Shared.Application.DaHua; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text.Json; + +namespace Common.Shared.DomainService +{ + /// + /// 获取大华icc平台的token服务 + /// + public class TokenProviderService : ITokenProviderService + { + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public TokenProviderService(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + /// + /// 开发测试的时候,忽略证书 + /// + private static readonly HttpClient _http = new(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }); + + public async Task GetTokenAsync(string clientId) + { + if (TokenCache.TokenMap.TryGetValue(clientId, out var tokenEntry) + && tokenEntry.ExpireAt > DateTimeOffset.UtcNow.AddMinutes(5)) + { + return tokenEntry.AccessToken!; + } + + var tokenLock = TokenLockProvider.GetLock(clientId); + await tokenLock.WaitAsync(); + try + { + // 加锁后再次检查,防止重复刷新 + if (TokenCache.TokenMap.TryGetValue(clientId, out tokenEntry) + && tokenEntry.ExpireAt > DateTimeOffset.UtcNow.AddMinutes(5)) + { + return tokenEntry.AccessToken!; + } + + var refreshed = await TryRefreshOrLoginAsync(clientId, tokenEntry); + return refreshed.AccessToken!; + } + finally + { + tokenLock.Release(); + } + } + + private async Task TryRefreshOrLoginAsync(string clientId, TokenEntry? current) + { + try + { + TokenEntry refreshed; + + if (current?.AccessToken is { } refreshToken) + { + var dto = new RefreshTokenReqDto + { + ClientId = clientId, + ClientSecret = _configuration["DahuaAuth:ClientSecret"]!, + GrantType = "refresh_token", + //刷新要求去掉 Bearer 前缀 + RefreshToken = refreshToken.Replace("Bearer ", string.Empty) + }; + + var result = await RefreshToken(dto); + + if (result?.Data != null && result.Data.AccessToken != "" && result.Data.AccessToken != null) + { + refreshed = new TokenEntry + { + AccessToken = string.Concat(result.Data.TokenType, " ", result.Data.AccessToken), + + ExpireAt = DateTimeOffset.UtcNow.AddSeconds(result.Data.ExpiresIn) + }; + _logger.LogInformation("Refresh 成功: {ClientId}", clientId); + } + else + { + _logger.LogWarning("RefreshToken 失败,尝试重新登录"); + refreshed = await GetDaHToken(); + } + } + else + { + refreshed = await GetDaHToken(); + } + + // 更新缓存 + TokenCache.TokenMap[clientId] = refreshed; + return refreshed; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取 token 异常:{ClientId}", clientId); + return new TokenEntry + { + AccessToken = string.Empty, + ExpireAt = DateTimeOffset.UtcNow.AddMinutes(1) + }; + } + } + + private async Task GetDaHToken() + { + //1. 获取公钥 + DaHApiResult publicKeyResult = await GetPublicKey(); + + LoginRequestDto dto = new(); + //2. 鉴权 + dto.PublicKey = publicKeyResult.Data.PublicKey; + dto.ClientId = _configuration["DahuaAuth:ClientId"]!; + dto.ClientSecret = _configuration["DahuaAuth:ClientSecret"]!; + dto.Password = _configuration["DahuaAuth:Password"]!; + dto.Username = _configuration["DahuaAuth:Username"]!; + + DaHApiResult loginResult = await GetToken(dto); + + TokenEntry refreshed = new() + { + AccessToken = loginResult.Data.AccessToken, + ExpireAt = DateTimeOffset.UtcNow.AddSeconds(120) + }; + return refreshed; + } + + /// + /// 刷新token,2个小时过期的 + /// + /// + /// + /// + /// + /// + private async Task> RefreshToken(RefreshTokenReqDto dto) + { + DaHApiResult result = new DaHApiResult() { Success = true, Code = "0" }; + + if (string.IsNullOrWhiteSpace(dto.RefreshToken)) + { + result.Success = false; + result.Code = "1005"; + result.Msg = "刷新令牌不能为空"; + _logger.LogWarning("刷新大华令牌失败,刷新令牌不能为空"); + return result; + } + + try + { + var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/extend/refresh/token"; + + using var resp = await _http.PostAsJsonAsync(url, dto); + resp.EnsureSuccessStatusCode(); + + result = await resp.Content.ReadFromJsonAsync>(); + + if (!result.Success || result.Code != "0") + { + result.Success = false; + result.Code = "1006"; + result.Msg = "刷新大华令牌失败"; + _logger.LogWarning("刷新大华令牌失败,返回结果:{Result}", result); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "刷新大华令牌出错"); + result.Success = false; + result.Code = "1006"; + result.Msg = "刷新大华令牌失败"; + } + return result; + } + + /// + /// 获取公钥 + /// + /// + /// + private async Task> GetPublicKey() + { + DaHApiResult result = new() { Success = true, Code = "0", Data = new PublicKeyDto() { PublicKey = "" } }; + try + { + var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/public-key"; + + using var resp = await _http.GetAsync(url); + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(); + var envelope = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (envelope?.Data?.PublicKey is null or { Length: 0 }) + { + _logger.LogWarning("获取大华公钥失败,返回结果:{Result}", json); + result.Success = false; + result.Code = "1001"; + result.Msg = "获取大华公钥失败"; + + return result; + } + + result.Data.PublicKey = envelope.Data.PublicKey; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "大华平台获取公钥出错"); + result.Success = false; + result.Code = "1001"; + result.Msg = "获取大华公钥失败"; + } + return result; + } + + /// + /// 获取token + /// + /// + /// + private async Task> GetToken(LoginRequestDto dto) + { + DaHApiResult result = new() { Success = true, Code = "0", Data = new LoginResDto { } }; + if (dto is null) + { + result.Success = false; + + result.Code = "1002"; + result.Msg = "请求参数不能为空"; + _logger.LogWarning("获取大华登录令牌失败,参数不能为空"); + return result; + } + if (string.IsNullOrWhiteSpace(dto.Password)) + { + result.Success = false; + result.Code = "1003"; + result.Msg = "密码不能为空"; + _logger.LogWarning("获取大华登录令牌失败,密码不能为空"); + return result; + } + try + { + var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/extend/token"; + //必须加密 + dto.Password = EncryptByPublicKey(dto.Password, dto.PublicKey!); + using var resp = await _http.PostAsJsonAsync(url, dto); + resp.EnsureSuccessStatusCode(); + + var tokenInfo = await resp.Content.ReadFromJsonAsync>(); + + if (tokenInfo == null || !result.Success || result.Code != "0") + { + result.Success = false; + result.Code = "1004"; + result.Msg = "获取大华登录令牌失败"; + _logger.LogWarning("获取大华登录令牌失败,返回结果:{Result}", result); + } + result = tokenInfo!; + //固定的拼接方式 + result.Data.AccessToken = string.Concat(tokenInfo?.Data.TokenType, " ", tokenInfo?.Data.AccessToken); + + TokenEntry refreshed = new TokenEntry + { + AccessToken = string.Concat(result!.Data.TokenType, " ", result.Data.AccessToken), + + ExpireAt = DateTimeOffset.UtcNow.AddSeconds(result.Data.ExpiresIn) + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取大华登录令牌出错"); + result.Success = false; + result.Code = "1004"; + result.Msg = "获取大华登录令牌失败"; + } + return result; + } + + #region RES加密 + + private static string EncryptByPublicKey(string context, string publicKey) + { + RSACryptoServiceProvider rsa = new(); + + rsa.ImportParameters(FromXmlStringExtensions(ConvertToXmlPublicJavaKey(publicKey))); + byte[] byteText = System.Text.Encoding.UTF8.GetBytes(context); + byte[] byteEntry = rsa.Encrypt(byteText, false); + return Convert.ToBase64String(byteEntry); + } + + private static RSAParameters FromXmlStringExtensions(string xmlString) + { + RSAParameters parameters = new RSAParameters(); + + System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument(); + xmlDoc.LoadXml(xmlString); + + if (xmlDoc.DocumentElement!.Name.Equals("RSAKeyValue")) + { + foreach (System.Xml.XmlNode node in xmlDoc.DocumentElement.ChildNodes) + { + switch (node.Name) + { + case "Modulus": parameters.Modulus = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "Exponent": parameters.Exponent = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "P": parameters.P = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "Q": parameters.Q = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "DP": parameters.DP = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "DQ": parameters.DQ = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "InverseQ": parameters.InverseQ = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + case "D": parameters.D = string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText); break; + } + } + } + else + { + throw new Exception("Invalid XML RSA key."); + } + + return parameters; + } + + private static string ConvertToXmlPublicJavaKey(string publicJavaKey) + { + RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicJavaKey)); + string xmlpublicKey = string.Format("{0}{1}", + Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()), + Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned())); + Console.WriteLine(xmlpublicKey); + return xmlpublicKey; + } + + #endregion RES加密 + } +} \ No newline at end of file diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/IMqttClientService.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/IMqttClientService.cs index 136e26a..e12fa0d 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/IMqttClientService.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/IMqttClientService.cs @@ -1,7 +1,6 @@ -using MQTTnet; -using MQTTnet.Client; +using MQTTnet.Client; -namespace Common.Shared.DomainService.MqttClient +namespace Common.Shared.DomainService { public interface IMqttClientService { diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MQTTClient.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MQTTClient.cs index 64addd6..6890e0d 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MQTTClient.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MQTTClient.cs @@ -4,18 +4,33 @@ using MQTTnet.Client; using MQTTnet.Protocol; using WeiCloud.Core.BaseModels; -namespace Common.Shared.DomainService.MqttClient +namespace Common.Shared.DomainService { public class MQTTClient { private readonly ILogger _logger; internal IMqttClient mqttClient; + public bool IsConnected => mqttClient != null && mqttClient.IsConnected; public MQTTClient(ILogger logger) { _logger = logger; } + /// + /// 如果未连接,则按参数调用 Init;成功后返回当前连接状态。 + /// + public async Task EnsureConnectedAsync( + string serverIp, int port, string authUser, string authPwd, + string topicNameStrs = "", string clientId = "") + { + if (mqttClient == null || !mqttClient.IsConnected) + { + await Init(serverIp, port, authUser, authPwd, topicNameStrs, clientId); + } + return mqttClient != null && mqttClient.IsConnected; + } + private async Task Subscribe(string topicNameStrs) { if (!string.IsNullOrWhiteSpace(topicNameStrs)) diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientListService.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientListService.cs index a5222f7..dd2379a 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientListService.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientListService.cs @@ -7,7 +7,7 @@ using System.Collections.Concurrent; using System.Text; using WeiCloud.Utils.Common; -namespace Common.Shared.DomainService.MqttClient +namespace Common.Shared.DomainService { public class MqttClientListService { diff --git a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientService.cs b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientService.cs index 912ffb1..1bde8c0 100644 --- a/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientService.cs +++ b/WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientService.cs @@ -1,9 +1,8 @@ -using Common.Shared.DomainService.MqttClient; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using MQTTnet; using MQTTnet.Client; -namespace WeiCloud.SafetyFirePro.Services +namespace Common.Shared.DomainService { public class MqttClientService : IMqttClientService { diff --git a/WeiCloud.Fusion/VideoService/Video.API/Controllers/DaHua/VideoManageController.cs b/WeiCloud.Fusion/VideoService/Video.API/Controllers/DaHua/VideoManageController.cs index 382937b..a830ca0 100644 --- a/WeiCloud.Fusion/VideoService/Video.API/Controllers/DaHua/VideoManageController.cs +++ b/WeiCloud.Fusion/VideoService/Video.API/Controllers/DaHua/VideoManageController.cs @@ -6,6 +6,9 @@ using WeiCloud.Core.BaseModels; namespace Video.API.Controllers.DaHua { + /// + /// 大华视频 + /// [Route("api/[controller]/[action]")] [ApiController] public class VideoManageController : ControllerBase @@ -14,6 +17,12 @@ namespace Video.API.Controllers.DaHua private readonly IRootVideoPlaybackService _rootVideoPlaybackService; private readonly IConfiguration _configuration; + /// + /// 构造 + /// + /// + /// + /// public VideoManageController(ILogger logger, IRootVideoPlaybackService rootVideoPlaybackService, IConfiguration configuration) { _logger = logger; @@ -23,17 +32,6 @@ namespace Video.API.Controllers.DaHua #region 大华视频处理 - /// - /// 大华视频的登录获取Token - /// - /// - /// - [HttpPost("token/dh")] - public async Task> GetDaHToken(LoginRequestDto dto) - { - return await _rootVideoPlaybackService.GetDaHToken(dto); - } - /// /// 大华视频回放 /// @@ -51,7 +49,7 @@ namespace Video.API.Controllers.DaHua /// /// [HttpPost("rtspplayback/dh")] - public async Task> RtspPlaybackByTime(PlaybackReqDto dto) + public async Task> RtspPlaybackByTime(RtspPlaybackReqDto dto) { return await _rootVideoPlaybackService.RtspPlaybackByTime(dto); } @@ -73,7 +71,7 @@ namespace Video.API.Controllers.DaHua /// /// [HttpPost("rtspstart/dh")] - public async Task> RtspStartVideoUrl(StreamReqDto dto) + public async Task> RtspStartVideoUrl(StreamRtspReqDto dto) { return await _rootVideoPlaybackService.RtspStartVideoUrl(dto); } diff --git a/WeiCloud.Fusion/VideoService/Video.API/HostService/DahuaTokenRefreshJob.cs b/WeiCloud.Fusion/VideoService/Video.API/HostService/DahuaTokenRefreshJob.cs deleted file mode 100644 index 1fd6bd0..0000000 --- a/WeiCloud.Fusion/VideoService/Video.API/HostService/DahuaTokenRefreshJob.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Quartz; -using Video.DomainService; - -namespace Video.API.HostService -{ - /// - /// 大华的token的刷新任务 - /// - public class DahuaTokenRefreshJob : IJob - { - private readonly ILogger _logger; - private readonly IDahuaGeneralCtlService _dahuaGeneralCtlService; - private readonly IConfiguration _configuration; - - public DahuaTokenRefreshJob(IDahuaGeneralCtlService dahuaGeneralCtlService, ILogger logger, IConfiguration configuration) - { - _dahuaGeneralCtlService = dahuaGeneralCtlService; - _logger = logger; - _configuration = configuration; - } - - public async Task Execute(IJobExecutionContext context) - { - if (_configuration["VideoOpen"] == "0") - { - if (TokenCache.TokenMap.Keys.Count > 0) - { - foreach (var clientId in TokenCache.TokenMap.Keys) - { - try - { - var token = await _dahuaGeneralCtlService.GetAccessTokenAsync(clientId); - _logger.LogInformation("刷新 token 成功:{ClientId} -> {Len}", clientId, token.Length); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "刷新 token 失败:{ClientId}", clientId); - Console.WriteLine(ex.Message); - } - } - } - else - { - var token = await _dahuaGeneralCtlService.GetAccessTokenAsync(_configuration["DahuaAuth:ClientId"]); - _logger.LogInformation("刷新 token 成功:{ClientId} -> {Len}", _configuration["DahuaAuth:ClientId"], token.Length); - } - } - } - } -} \ No newline at end of file diff --git a/WeiCloud.Fusion/VideoService/Video.API/Program.cs b/WeiCloud.Fusion/VideoService/Video.API/Program.cs index dfb300b..e58b492 100644 --- a/WeiCloud.Fusion/VideoService/Video.API/Program.cs +++ b/WeiCloud.Fusion/VideoService/Video.API/Program.cs @@ -1,13 +1,12 @@ using Autofac; using Autofac.Extensions.DependencyInjection; +using Common.Shared.DomainService; using Microsoft.OpenApi.Models; using NLog; using NLog.Extensions.Logging; using NLog.Web; using Quartz; -using Quartz.Simpl; using System.Reflection; -using Video.API.HostService; using Video.API.Infrastructure; using Video.Application; @@ -113,31 +112,18 @@ namespace Video.API #endregion CAPע - #region ʱ - - builder.Services.AddQuartz(q => - { - q.UseJobFactory(); - - // DahuaToken ˢ - var jobKey2 = new JobKey("DahuaTokenRefresh"); - q.AddJob(opts => opts.WithIdentity(jobKey2)); - q.AddTrigger(opts => opts - .ForJob(jobKey2) - .WithIdentity("DahuaTokenRefresh-trigger") - .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMinutes(120)).RepeatForever())); - }); - - builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); - - #endregion ʱ - // ȫĬС builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = 200 * 1024 * 1024; // Ĭ 200MB }); + #region עtoken + + builder.Services.AddSingleton(); + + #endregion עtoken + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/WeiCloud.Fusion/VideoService/Video.API/Video.API.csproj b/WeiCloud.Fusion/VideoService/Video.API/Video.API.csproj index e36b91d..77b0250 100644 --- a/WeiCloud.Fusion/VideoService/Video.API/Video.API.csproj +++ b/WeiCloud.Fusion/VideoService/Video.API/Video.API.csproj @@ -9,7 +9,6 @@ - diff --git a/WeiCloud.Fusion/VideoService/Video.API/appsettings.json b/WeiCloud.Fusion/VideoService/Video.API/appsettings.json index 22333bd..f58cfc9 100644 --- a/WeiCloud.Fusion/VideoService/Video.API/appsettings.json +++ b/WeiCloud.Fusion/VideoService/Video.API/appsettings.json @@ -25,10 +25,12 @@ "VideoOpen": "1", //0表示部署视频对接,1表示不对接 //大华摄像头的配置 "DahuaAuth": { - "Host": "v4.weienergy.cn", - "ClientId": "test", - "ClientSecret": "", - "Username": "", - "Password": "" + "Host": "demo.weienergy.cn:15214", + "ClientId": "taiyanggong", + + "ClientSecret": "6d6c78f8-3d4c-4e76-ab6b-827942a7b725", + + "Username": "system", + "Password": "Admin123" } } \ No newline at end of file diff --git a/WeiCloud.Fusion/VideoService/Video.Application/RequestDto/DahuaVideoQueryDto.cs b/WeiCloud.Fusion/VideoService/Video.Application/RequestDto/DahuaVideoQueryDto.cs index f272dad..f597276 100644 --- a/WeiCloud.Fusion/VideoService/Video.Application/RequestDto/DahuaVideoQueryDto.cs +++ b/WeiCloud.Fusion/VideoService/Video.Application/RequestDto/DahuaVideoQueryDto.cs @@ -6,39 +6,57 @@ using System.Text.Json.Serialization; namespace Video.Application { /// - /// 刷新 access_token 的请求参数模型 + /// hls、rtmp回放请求的数据部分 /// - public class RefreshTokenReqDto + public class PlaybackReqDto { /// - /// 认证类型,固定值:refresh_token + /// 通道ID(格式如:1000018$1$0$0) /// - [JsonPropertyName("grant_type")] - public string GrantType { get; set; } = "refresh_token"; // 默认值,通常固定 + [JsonPropertyName("channelId")] + public string ChannelId { get; set; } /// - /// 客户端ID(与认证接口中一致) + /// 流类型:1-主码流,2-子码流(通常为字符串或数字) + /// + [JsonPropertyName("streamType")] + public string StreamType { get; set; } + + /// + /// 输出类型,如 "hls"、"rtmp" 等,如果RTSP的回放,不加此字段 /// - [JsonPropertyName("client_id")] - public string ClientId { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } = "hls"; /// - /// 客户端密钥(与认证接口中一致) + /// 录像类型:1-定时录像,2-移动侦测,3-报警录像等(字符串形式) /// - [JsonPropertyName("client_secret")] - public string ClientSecret { get; set; } + [JsonPropertyName("recordType")] + public string RecordType { get; set; } /// - /// 刷新令牌(refresh_token),用于获取新的 access_token + /// 回放开始时间,格式:"yyyy-M-d HH:mm:ss" /// - [JsonPropertyName("refresh_token")] - public string RefreshToken { get; set; } + [JsonPropertyName("beginTime")] + public string BeginTime { get; set; } + + /// + /// 回放结束时间,格式:"yyyy-M-d HH:mm:ss" + /// + [JsonPropertyName("endTime")] + public string EndTime { get; set; } + + /// + /// 录像来源:1-设备,2-平台,3-云端等 + /// + [JsonPropertyName("recordSource")] + public string RecordSource { get; set; } } /// - /// 回放请求的数据部分 + /// hls、rtmp回放请求的数据部分 /// - public class PlaybackReqDto + public class RtspPlaybackReqDto { /// /// 通道ID(格式如:1000018$1$0$0) @@ -52,12 +70,6 @@ namespace Video.Application [JsonPropertyName("streamType")] public string StreamType { get; set; } - /// - /// 输出类型,如 "hls"、"rtmp" 等,如果RTSP的回放,不加此字段 - /// - [JsonPropertyName("type")] - public string? Type { get; set; } = "hls"; - /// /// 录像类型:1-定时录像,2-移动侦测,3-报警录像等(字符串形式) /// @@ -67,8 +79,8 @@ namespace Video.Application /// /// 回放开始时间,格式:"yyyy-M-d HH:mm:ss" /// - [JsonPropertyName("beginTime")] - public string BeginTime { get; set; } + [JsonPropertyName("startTime")] + public string startTime { get; set; } /// /// 回放结束时间,格式:"yyyy-M-d HH:mm:ss" @@ -81,11 +93,6 @@ namespace Video.Application /// [JsonPropertyName("recordSource")] public string RecordSource { get; set; } - - /// - /// 鉴权的token - /// - public string? Token { get; set; } } /// @@ -223,29 +230,54 @@ namespace Video.Application /// [JsonPropertyName("type")] public string? Type { get; set; } + } + /// + /// 实时 流播放请求响应包装类 + /// + public class StreamReqDto + { /// - /// rtsp专用,有datatype没有type,有type没有datatype - /// - [JsonPropertyName("dataType")] - public string? DataType { get; set; } - - /// - /// 请求头认证 + /// 请求数据 /// - public string? Token { get; set; } + [JsonPropertyName("data")] + public StreamRequestData Data { get; set; } } /// /// 实时 流播放请求响应包装类 /// - public class StreamReqDto + public class StreamRtspReqDto { /// /// 请求数据 /// [JsonPropertyName("data")] - public StreamRequestData Data { get; set; } + public StreamRtspRequestData Data { get; set; } + } + + /// + /// 实时流播放请求数据实体 + /// + public class StreamRtspRequestData + { + /// + /// 通道编码 + /// + [JsonPropertyName("channelId")] + public string ChannelId { get; set; } + + /// + /// 码流类型:1-主码流,2-子码流 + /// + [JsonPropertyName("streamType")] + public string StreamType { get; set; } + + /// + /// rtsp专用,有datatype没有type,有type没有datatype + /// + [JsonPropertyName("dataType")] + public string? DataType { get; set; } } /// diff --git a/WeiCloud.Fusion/VideoService/Video.Application/ResponeDto/DahuaVideoResDto.cs b/WeiCloud.Fusion/VideoService/Video.Application/ResponeDto/DahuaVideoResDto.cs index fc82b08..76f9bc5 100644 --- a/WeiCloud.Fusion/VideoService/Video.Application/ResponeDto/DahuaVideoResDto.cs +++ b/WeiCloud.Fusion/VideoService/Video.Application/ResponeDto/DahuaVideoResDto.cs @@ -305,7 +305,92 @@ namespace Video.Application /// public class UrlDataDto { + /// + /// 最小速率 + /// + [JsonPropertyName("minRate")] + public object MinRate { get; set; } // 用object类型兼容null和可能的数值类型 + + /// + /// 协议类型 + /// + [JsonPropertyName("protocol")] + public string Protocol { get; set; } // 可为null + + /// + /// IP地址 + /// + [JsonPropertyName("ip")] + public string Ip { get; set; } // 可为null + + /// + /// 端口号 + /// + [JsonPropertyName("port")] + public object Port { get; set; } // 用object类型兼容null和可能的数值类型 + + /// + /// STUN启用状态 + /// + [JsonPropertyName("stunEnable")] + public bool? StunEnable { get; set; } // 可空布尔类型 + + /// + /// STUN端口 + /// + [JsonPropertyName("stunPort")] + public object StunPort { get; set; } // 用object类型兼容null和可能的数值类型 + + /// + /// RTSP地址 + /// [JsonPropertyName("url")] public string Url { get; set; } + + /// + /// 连接类型 + /// + [JsonPropertyName("connectType")] + public string ConnectType { get; set; } // 可为null + + /// + /// 会话标识 + /// + [JsonPropertyName("session")] + public string Session { get; set; } + + /// + /// 令牌 + /// + [JsonPropertyName("token")] + public string Token { get; set; } + + /// + /// 轨道标识 + /// + [JsonPropertyName("trackId")] + public string TrackId { get; set; } // 可为null + + // 添加JSON中存在的新属性 + [JsonPropertyName("urlList")] + public object UrlList { get; set; } // 可为null + + [JsonPropertyName("stream")] + public object Stream { get; set; } // 可为null + + [JsonPropertyName("innerIp")] + public string InnerIp { get; set; } // 新增IP属性 + + [JsonPropertyName("compress")] + public bool? Compress { get; set; } // 压缩标识 + + [JsonPropertyName("reachable")] + public object Reachable { get; set; } // 可为null + + [JsonPropertyName("wssDirect")] + public int? WssDirect { get; set; } // 新增数值属性 + + [JsonPropertyName("netFlag")] + public string NetFlag { get; set; } // 网络标识 } } \ No newline at end of file diff --git a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/DahuaGeneralCtlService.cs b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/DahuaGeneralCtlService.cs index 993c9ce..81d127f 100644 --- a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/DahuaGeneralCtlService.cs +++ b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/DahuaGeneralCtlService.cs @@ -1,10 +1,8 @@ using Common.Shared.Application.DaHua; +using Common.Shared.DomainService; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Security; using System.Net.Http.Json; -using System.Security.Cryptography; using System.Text.Json; using Video.Application; @@ -14,108 +12,25 @@ namespace Video.DomainService { private readonly ILogger _logger; private readonly IConfiguration _configuration; - private readonly HttpClient _http; + private readonly ITokenProviderService _tokenProviderService; + // private readonly HttpClient _http; - public DahuaGeneralCtlService(ILogger logger, IConfiguration configuration, HttpClient http) + public DahuaGeneralCtlService(ILogger logger, IConfiguration configuration, ITokenProviderService tokenProviderService) { _logger = logger; _configuration = configuration; - _http = http; - } - - /// - /// 获取公钥 - /// - /// - /// - public async Task> GetPublicKey() - { - DaHApiResult result = new() { Success = true, Code = "0", Data = new PublicKeyDto() { PublicKey = "" } }; - try - { - var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/public-key"; + _tokenProviderService = tokenProviderService; - using var resp = await _http.GetAsync(url); - resp.EnsureSuccessStatusCode(); - var json = await resp.Content.ReadAsStringAsync(); - var envelope = JsonSerializer.Deserialize>(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - if (envelope?.Data?.PublicKey is null or { Length: 0 }) - { - _logger.LogWarning("获取大华公钥失败,返回结果:{Result}", json); - result.Success = false; - result.Code = "1001"; - result.Msg = "获取大华公钥失败"; - - return result; - } - - result.Data.PublicKey = envelope.Data.PublicKey; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "大华平台获取公钥出错"); - } - return result; + //_http = http; } /// - /// 获取token + /// 开发测试的时候,忽略证书 /// - /// - /// - public async Task> GetToken(LoginRequestDto dto) + private static readonly HttpClient _http = new HttpClient(new HttpClientHandler { - DaHApiResult result = new() { Success = true, Code = "0" }; - if (dto is null) - { - result.Success = false; - - result.Code = "1002"; - result.Msg = "请求参数不能为空"; - _logger.LogWarning("获取大华登录令牌失败,参数不能为空"); - return result; - } - if (string.IsNullOrWhiteSpace(dto.Password)) - { - result.Success = false; - result.Code = "1003"; - result.Msg = "密码不能为空"; - _logger.LogWarning("获取大华登录令牌失败,密码不能为空"); - return result; - } - try - { - var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/extend/token"; - //必须加密 - dto.Password = EncryptByPublicKey(dto.Password, dto.PublicKey!); - using var resp = await _http.PostAsJsonAsync(url, dto); - resp.EnsureSuccessStatusCode(); - - var tokenInfo = await resp.Content.ReadFromJsonAsync>(); - - if (tokenInfo == null || !result.Success || result.Code != "0") - { - result.Success = false; - result.Code = "1004"; - result.Msg = "获取大华登录令牌失败"; - _logger.LogWarning("获取大华登录令牌失败,返回结果:{Result}", result); - } - //固定的拼接方式 - result.Data.AccessToken = string.Concat(tokenInfo!.Data.TokenType, " ", tokenInfo.Data.AccessToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "获取大华登录令牌出错"); - result.Success = false; - result.Code = "1004"; - result.Msg = "获取大华登录令牌失败"; - } - return result; - } + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }); /// /// 录像回放 @@ -136,9 +51,7 @@ namespace Video.DomainService { // 2) Token:优先入参,其次缓存/获取(建议返回完整的 "Bearer xxx") var clientId = _configuration["DahuaAuth:ClientId"]; - var token = string.IsNullOrWhiteSpace(dto.Token) - ? await GetCachedOrFetchTokenAsync(clientId) - : dto.Token; + var token = await _tokenProviderService.GetTokenAsync(clientId!); var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/admin/API/video/stream/record"; @@ -190,9 +103,7 @@ namespace Video.DomainService } var clientId = _configuration["DahuaAuth:ClientId"]; - var token = string.IsNullOrWhiteSpace(dto.Token) - ? await GetCachedOrFetchTokenAsync(clientId) // 建议用这个轻量封装;返回完整 "Bearer xxx" - : dto.Token; + var token = await _tokenProviderService.GetTokenAsync(clientId!); var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/admin/API/SS/Record/QueryRecords"; @@ -229,54 +140,6 @@ namespace Video.DomainService } } - /// - /// 刷新token,2个小时过期的 - /// - /// - /// - /// - /// - /// - public async Task> RefreshToken(RefreshTokenReqDto dto) - { - DaHApiResult result = new DaHApiResult() { Success = true, Code = "0" }; - - if (string.IsNullOrWhiteSpace(dto.RefreshToken)) - { - result.Success = false; - result.Code = "1005"; - result.Msg = "刷新令牌不能为空"; - _logger.LogWarning("刷新大华令牌失败,刷新令牌不能为空"); - return result; - } - - try - { - var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-oauth/1.0.0/oauth/extend/refresh/token"; - - using var resp = await _http.PostAsJsonAsync(url, dto); - resp.EnsureSuccessStatusCode(); - - result = await resp.Content.ReadFromJsonAsync>(); - - if (!result.Success || result.Code != "0") - { - result.Success = false; - result.Code = "1006"; - result.Msg = "刷新大华令牌失败"; - _logger.LogWarning("刷新大华令牌失败,返回结果:{Result}", result); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "刷新大华令牌出错"); - result.Success = false; - result.Code = "1006"; - result.Msg = "刷新大华令牌失败"; - } - return result; - } - /// /// 设备通道分页查询 /// @@ -293,9 +156,7 @@ namespace Video.DomainService } var clientId = _configuration["DahuaAuth:ClientId"]; - var token = string.IsNullOrWhiteSpace(dto.Token) - ? await GetCachedOrFetchTokenAsync(clientId) // 建议统一用这个轻量封装 - : dto.Token; // 约定这里是完整 "Bearer xxx" + var token = await _tokenProviderService.GetTokenAsync(clientId!); var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/evo-brm/1.2.0/device/channel/subsystem/page"; @@ -351,9 +212,7 @@ namespace Video.DomainService { // 2) Token:优先用入参;否则走缓存/获取(建议返回已带前缀的 "Bearer xxx") var clientId = _configuration["DahuaAuth:ClientId"]; - var token = string.IsNullOrWhiteSpace(dto.Data.Token) - ? await GetCachedOrFetchTokenAsync(clientId) // 见下方简版实现 - : dto.Data.Token; + var token = await _tokenProviderService.GetTokenAsync(clientId!); var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/admin/API/video/stream/realtime"; @@ -440,43 +299,13 @@ namespace Video.DomainService } } - /// - /// 获取AccessToken - /// - /// - /// - /// - /// - public async Task GetAccessTokenAsync(string? clientId, CancellationToken ct = default) - { - try - { - TokenEntry refreshed = new(); - if (clientId == null || clientId == "") - { - clientId = _configuration["DahuaAuth:ClientId"]; - } - - if (!TokenCache.TokenMap.TryGetValue(clientId!, out var entry) || TokenIsExpiring(entry)) - { - refreshed = await TryRefreshOrLoginAsync(clientId!, entry); - } - return refreshed.AccessToken; - } - catch (Exception ex) - { - _logger.LogError(ex, "获取大华AccessToken出错"); - return string.Empty; // 返回空字符串表示获取失败 - } - } - /// /// rtsp录像回放 /// /// /// /// - public async Task> RtspPlaybackByTime(PlaybackReqDto dto) + public async Task> RtspPlaybackByTime(RtspPlaybackReqDto dto) { // 参数校验 + 早退 if (dto == null || string.IsNullOrWhiteSpace(dto.ChannelId)) @@ -487,11 +316,9 @@ namespace Video.DomainService // 先用缓存里的 token,不足5分钟过期再刷新(按你之前的口径来) var clientId = _configuration["DahuaAuth:ClientId"]; - var token = string.IsNullOrWhiteSpace(dto.Token) - ? await GetCachedOrFetchTokenAsync(clientId) - : dto.Token; + var token = await _tokenProviderService.GetTokenAsync(clientId!); - var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/admin/API/video/stream/record"; + var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/admin/API/SS/Playback/StartPlaybackByTime"; using var req = new HttpRequestMessage(HttpMethod.Post, url) { @@ -532,7 +359,7 @@ namespace Video.DomainService /// /// /// - public async Task> RtspStartVideoUrl(StreamReqDto dto) + public async Task> RtspStartVideoUrl(StreamRtspReqDto dto) { if (dto == null || dto.Data == null) { @@ -541,9 +368,7 @@ namespace Video.DomainService } var clientId = _configuration["DahuaAuth:ClientId"]; - var token = string.IsNullOrWhiteSpace(dto.Data.Token) - ? await GetCachedOrFetchTokenAsync(clientId) - : dto.Data.Token; + var token = await _tokenProviderService.GetTokenAsync(clientId!); var url = $"https://{_configuration["DahuaAuth:Host"]}/evo-apigw/admin/API/MTS/Video/StartVideo"; @@ -565,7 +390,7 @@ namespace Video.DomainService } var result = JsonSerializer.Deserialize>(body); - if (result == null || !result.Success || result.Code != "0") + if (result == null || !result.Success || result.Code != "100") { _logger.LogWarning("实时流请求业务失败: {Body}", body); return new DaHApiResult { Success = false, Code = "1010", Msg = "实时流请求失败" }; @@ -579,171 +404,5 @@ namespace Video.DomainService return new DaHApiResult { Success = false, Code = "1010", Msg = "实时流请求失败" }; } } - - #region GetToken的辅助方法 - - private async Task GetCachedOrFetchTokenAsync(string clientId) - { - if (TokenCache.TokenMap.TryGetValue(clientId, out var e) && e != null && e.ExpireAt > DateTimeOffset.UtcNow.AddMinutes(5)) - return e.AccessToken; // 缓存可用 - - var refreshed = await GetAccessTokenAsync(clientId); - return refreshed; - } - - private static bool TokenIsExpiring(TokenEntry entry) - { - return entry.ExpireAt <= DateTimeOffset.UtcNow.AddMinutes(5); - } - - private async Task TryRefreshOrLoginAsync(string clientId, TokenEntry? current) - { - try - { - TokenEntry refreshed; - - if (current != null) - { - RefreshTokenReqDto refreshTokenReqDto = new() - { - ClientId = clientId, - RefreshToken = current.RefreshToken, - ClientSecret = _configuration["DahuaAuth:ClientSecret"], - GrantType = "refresh_token" - }; - //刷新token - var result = await this.RefreshToken(refreshTokenReqDto); - refreshed = new TokenEntry - { - AccessToken = result.Data.AccessToken, - RefreshToken = result.Data.RefreshToken, - ExpireAt = DateTimeOffset.UtcNow.AddSeconds(result.Data.ExpiresIn) - }; - _logger.LogWarning("Refresh 成功: {ClientId}", clientId); - } - else - { - var publicKeyResult = await this.GetPublicKey(); - if (publicKeyResult != null && publicKeyResult.Data != null) - { - _logger.LogWarning("获取公钥成功: {PublicKey}", publicKeyResult.Data.PublicKey); - - LoginRequestDto loginRequestDto = new() - { - ClientId = clientId, - ClientSecret = _configuration["DahuaAuth:ClientSecret"], - Username = _configuration["DahuaAuth:Username"], - Password = _configuration["DahuaAuth:Password"], - PublicKey = publicKeyResult.Data.PublicKey, - GrantType = "password", - VerifyCodeFlag = 0 // 默认不开启动态验证码 - }; - - var result = await this.GetToken(loginRequestDto); - if (result.Data != null) - { - refreshed = new TokenEntry - { - AccessToken = string.Concat(result!.Data.TokenType, " ", result.Data.AccessToken), - RefreshToken = result.Data.RefreshToken, - ExpireAt = DateTimeOffset.UtcNow.AddSeconds(result.Data.ExpiresIn) - }; - _logger.LogWarning("Login 成功: {ClientId}", clientId); - } - else - { - _logger.LogWarning("获取公钥失败"); - return new TokenEntry - { - AccessToken = string.Empty, - RefreshToken = string.Empty, - ExpireAt = DateTimeOffset.UtcNow.AddMinutes(5) // 设置一个短期的过期时间,避免后续调用失败 - }; - } - } - else - { - _logger.LogWarning("获取公钥失败"); - return new TokenEntry - { - AccessToken = string.Empty, - RefreshToken = string.Empty, - ExpireAt = DateTimeOffset.UtcNow.AddMinutes(5) // 设置一个短期的过期时间,避免后续调用失败 - }; - } - } - - TokenCache.TokenMap[clientId] = refreshed; - return refreshed; - } - catch (Exception ex) - { - _logger.LogError(ex, "获取 token 失败:{ClientId}", clientId); - return new TokenEntry - { - AccessToken = string.Empty, - RefreshToken = string.Empty, - ExpireAt = DateTimeOffset.UtcNow.AddMinutes(5) // 设置一个短期的过期时间,避免后续调用失败 - }; - } - } - - #endregion GetToken的辅助方法 - - #region RES加密 - - private static String EncryptByPublicKey(String context, String publicKey) - { - RSACryptoServiceProvider rsa = new(); - - rsa.ImportParameters(FromXmlStringExtensions(ConvertToXmlPublicJavaKey(publicKey))); - byte[] byteText = System.Text.Encoding.UTF8.GetBytes(context); - byte[] byteEntry = rsa.Encrypt(byteText, false); - return Convert.ToBase64String(byteEntry); - } - - public static RSAParameters FromXmlStringExtensions(string xmlString) - { - RSAParameters parameters = new RSAParameters(); - - System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument(); - xmlDoc.LoadXml(xmlString); - - if (xmlDoc.DocumentElement!.Name.Equals("RSAKeyValue")) - { - foreach (System.Xml.XmlNode node in xmlDoc.DocumentElement.ChildNodes) - { - switch (node.Name) - { - case "Modulus": parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "Exponent": parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "P": parameters.P = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "Q": parameters.Q = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "DP": parameters.DP = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "DQ": parameters.DQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "InverseQ": parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - case "D": parameters.D = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; - } - } - } - else - { - throw new Exception("Invalid XML RSA key."); - } - - return parameters; - } - - public static string ConvertToXmlPublicJavaKey(string publicJavaKey) - { - RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicJavaKey)); - string xmlpublicKey = string.Format("{0}{1}", - Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()), - Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned())); - Console.WriteLine(xmlpublicKey); - return xmlpublicKey; - } - - #endregion RES加密 } } \ No newline at end of file diff --git a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IDahuaGeneralCtlService.cs b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IDahuaGeneralCtlService.cs index 9d48e1a..257ae7a 100644 --- a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IDahuaGeneralCtlService.cs +++ b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IDahuaGeneralCtlService.cs @@ -8,26 +8,6 @@ namespace Video.DomainService /// public interface IDahuaGeneralCtlService { - /// - /// 获取公钥 - /// - /// - Task> GetPublicKey(); - - /// - /// 鉴权 - /// - /// PublicKey - /// - Task> GetToken(LoginRequestDto dto); - - /// - /// 刷新token - /// - /// - /// - Task> RefreshToken(RefreshTokenReqDto dto); - /// /// 查询普通录像信息列表(后续可能用于hls的拼接) /// @@ -47,7 +27,7 @@ namespace Video.DomainService /// /// /// - Task> RtspPlaybackByTime(PlaybackReqDto dto); + Task> RtspPlaybackByTime(RtspPlaybackReqDto dto); /// /// 设备通道分页查询,需要用于HlsRecordVideo @@ -69,20 +49,12 @@ namespace Video.DomainService /// /// /// - Task> RtspStartVideoUrl(StreamReqDto dto); + Task> RtspStartVideoUrl(StreamRtspReqDto dto); /// /// 注销认证信息 /// 没有返回值 /// Task> Logout(string authorization, string? openId, int? userClient); - - /// - /// 根据 clientId 获取当前可用 token(自动处理过期) - /// - /// 如果不传就从appsetting中得到 - /// - /// - Task GetAccessTokenAsync(string? clientId, CancellationToken ct = default); } } \ No newline at end of file diff --git a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IRootVideoPlaybackService.cs b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IRootVideoPlaybackService.cs index 2e1193c..bf18574 100644 --- a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IRootVideoPlaybackService.cs +++ b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IRootVideoPlaybackService.cs @@ -13,13 +13,6 @@ namespace Video.DomainService /// Task> GetDaHRecordVideoUrl(PlaybackReqDto dto); - /// - /// 大华的token获取 - /// - /// - /// - Task> GetDaHToken(LoginRequestDto dto); - /// /// 大华的实时视频 /// @@ -32,14 +25,14 @@ namespace Video.DomainService /// /// /// - Task> RtspStartVideoUrl(StreamReqDto dto); + Task> RtspStartVideoUrl(StreamRtspReqDto dto); /// /// rtsp录像回放 /// /// /// - Task> RtspPlaybackByTime(PlaybackReqDto dto); + Task> RtspPlaybackByTime(RtspPlaybackReqDto dto); /// /// 大华设备通道分页查询 diff --git a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/RootVideoPlaybackService.cs b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/RootVideoPlaybackService.cs index c37f47f..fe78aaa 100644 --- a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/RootVideoPlaybackService.cs +++ b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/RootVideoPlaybackService.cs @@ -50,45 +50,6 @@ namespace Video.DomainService return result; } - /// - /// 大华的鉴权 - /// - /// - /// - public async Task> GetDaHToken(LoginRequestDto dto) - { - ApiResult result = new() { Code = 200, Msg = "接口调用成功" }; - //1. 获取公钥 - DaHApiResult publicKeyResult = await _dahuaGeneralCtlService.GetPublicKey(); - if (!publicKeyResult.Success) - { - result.Code = 500; - result.Msg = publicKeyResult.Msg; - _logger.LogWarning("获取大华公钥失败:{Msg}", publicKeyResult.Msg); - } - //2. 鉴权 - dto.PublicKey = publicKeyResult.Data.PublicKey; - - DaHApiResult loginResult = await _dahuaGeneralCtlService.GetToken(dto); - if (!loginResult.Success) - { - result.Code = 500; - result.Msg = loginResult.Msg; - _logger.LogWarning("大华鉴权失败:{Msg}", loginResult.Msg); - return result; - } - //大华的规则 - result.Data = loginResult.Data.AccessToken; - TokenEntry refreshed = new TokenEntry - { - AccessToken = result.Data, - RefreshToken = result.Data, - ExpireAt = DateTimeOffset.UtcNow.AddSeconds(120) - }; - TokenCache.TokenMap[dto.ClientId] = refreshed; - return result; - } - /// /// 大华实时 /// @@ -160,7 +121,7 @@ namespace Video.DomainService /// /// /// - public async Task> RtspStartVideoUrl(StreamReqDto dto) + public async Task> RtspStartVideoUrl(StreamRtspReqDto dto) { ApiResult result = new ApiResult() { Code = 200, Msg = "接口调用成功" }; var urlReult = await _dahuaGeneralCtlService.RtspStartVideoUrl(dto); @@ -179,7 +140,7 @@ namespace Video.DomainService /// /// /// - public async Task> RtspPlaybackByTime(PlaybackReqDto dto) + public async Task> RtspPlaybackByTime(RtspPlaybackReqDto dto) { ApiResult result = new ApiResult() { Code = 200, Msg = "接口调用成功" }; diff --git a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/TokenCacheService.cs b/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/TokenCacheService.cs deleted file mode 100644 index 80801fc..0000000 --- a/WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/TokenCacheService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Concurrent; - -namespace Video.DomainService -{ - public static class TokenCache - { - public static readonly ConcurrentDictionary TokenMap = new(); - } - - /// - /// 用于token缓存的条目类 - /// - public class TokenEntry - { - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public DateTimeOffset ExpireAt { get; set; } - } -} \ No newline at end of file diff --git a/WeiCloud.Fusion/VideoService/Video.DomainService/Video.DomainService.csproj b/WeiCloud.Fusion/VideoService/Video.DomainService/Video.DomainService.csproj index 9866c24..7b3b365 100644 --- a/WeiCloud.Fusion/VideoService/Video.DomainService/Video.DomainService.csproj +++ b/WeiCloud.Fusion/VideoService/Video.DomainService/Video.DomainService.csproj @@ -8,6 +8,7 @@ + diff --git a/WeiCloud.Fusion/WeiCloud.Fusion.sln b/WeiCloud.Fusion/WeiCloud.Fusion.sln index b0e6721..16a8e93 100644 --- a/WeiCloud.Fusion/WeiCloud.Fusion.sln +++ b/WeiCloud.Fusion/WeiCloud.Fusion.sln @@ -47,16 +47,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AlarmService", "AlarmServic EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlarmService.API", "AlarmService\AlarmService.API\AlarmService.API.csproj", "{2677EAF0-9F7F-4969-B8B1-3006F35EB93E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alarm.DomainService", "Alarm.DomainService\Alarm.DomainService.csproj", "{3ED553C4-3A63-4613-B979-472FDA5EA346}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common.SharedService", "Common.SharedService", "{80F3B34B-C334-44D2-A861-31FD403AD57D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alarm.Application", "Alarm.Application\Alarm.Application.csproj", "{89367194-A636-41B9-81F0-283DCB84C296}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Shared.Application", "Common.SharedService\Common.Shared.Application\Common.Shared.Application.csproj", "{9A5FBAFF-EBE8-3156-5547-FB3ED1DEB545}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Shared.DomainService", "Common.SharedService\Common.Shared.DomainService\Common.Shared.DomainService.csproj", "{C2757FC0-54A9-BBD3-2E23-55F2F3912BA4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alarm.DomainService", "AlarmService\Alarm.DomainService\Alarm.DomainService.csproj", "{B6DDF83D-591E-38B6-2902-1624BE8AE9B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alarm.Application", "AlarmService\Alarm.Application\Alarm.Application.csproj", "{4B2C6EBE-E719-9F40-ADE6-C82DA632E554}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Shared.API", "Common.SharedService\Common.Shared.API\Common.Shared.API.csproj", "{1ACFAAE8-C86D-4582-B0B4-542B74970737}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,14 +125,6 @@ Global {2677EAF0-9F7F-4969-B8B1-3006F35EB93E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2677EAF0-9F7F-4969-B8B1-3006F35EB93E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2677EAF0-9F7F-4969-B8B1-3006F35EB93E}.Release|Any CPU.Build.0 = Release|Any CPU - {3ED553C4-3A63-4613-B979-472FDA5EA346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3ED553C4-3A63-4613-B979-472FDA5EA346}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3ED553C4-3A63-4613-B979-472FDA5EA346}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3ED553C4-3A63-4613-B979-472FDA5EA346}.Release|Any CPU.Build.0 = Release|Any CPU - {89367194-A636-41B9-81F0-283DCB84C296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89367194-A636-41B9-81F0-283DCB84C296}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89367194-A636-41B9-81F0-283DCB84C296}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89367194-A636-41B9-81F0-283DCB84C296}.Release|Any CPU.Build.0 = Release|Any CPU {9A5FBAFF-EBE8-3156-5547-FB3ED1DEB545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9A5FBAFF-EBE8-3156-5547-FB3ED1DEB545}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A5FBAFF-EBE8-3156-5547-FB3ED1DEB545}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -139,6 +133,18 @@ Global {C2757FC0-54A9-BBD3-2E23-55F2F3912BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2757FC0-54A9-BBD3-2E23-55F2F3912BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2757FC0-54A9-BBD3-2E23-55F2F3912BA4}.Release|Any CPU.Build.0 = Release|Any CPU + {B6DDF83D-591E-38B6-2902-1624BE8AE9B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6DDF83D-591E-38B6-2902-1624BE8AE9B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6DDF83D-591E-38B6-2902-1624BE8AE9B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6DDF83D-591E-38B6-2902-1624BE8AE9B9}.Release|Any CPU.Build.0 = Release|Any CPU + {4B2C6EBE-E719-9F40-ADE6-C82DA632E554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B2C6EBE-E719-9F40-ADE6-C82DA632E554}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B2C6EBE-E719-9F40-ADE6-C82DA632E554}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B2C6EBE-E719-9F40-ADE6-C82DA632E554}.Release|Any CPU.Build.0 = Release|Any CPU + {1ACFAAE8-C86D-4582-B0B4-542B74970737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ACFAAE8-C86D-4582-B0B4-542B74970737}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ACFAAE8-C86D-4582-B0B4-542B74970737}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ACFAAE8-C86D-4582-B0B4-542B74970737}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -159,10 +165,11 @@ Global {40B0D902-553C-C52F-71A2-56FB176FCCBD} = {44DAA396-C724-480A-A2BC-9A33D29E8FEA} {9F2BD2C5-6496-419D-B87A-4F481E963C4D} = {19A25984-FFA8-49BE-A710-6F269A406C61} {2677EAF0-9F7F-4969-B8B1-3006F35EB93E} = {18791734-CA81-482D-964A-CA6D0F308B8E} - {3ED553C4-3A63-4613-B979-472FDA5EA346} = {18791734-CA81-482D-964A-CA6D0F308B8E} - {89367194-A636-41B9-81F0-283DCB84C296} = {18791734-CA81-482D-964A-CA6D0F308B8E} {9A5FBAFF-EBE8-3156-5547-FB3ED1DEB545} = {80F3B34B-C334-44D2-A861-31FD403AD57D} {C2757FC0-54A9-BBD3-2E23-55F2F3912BA4} = {80F3B34B-C334-44D2-A861-31FD403AD57D} + {B6DDF83D-591E-38B6-2902-1624BE8AE9B9} = {18791734-CA81-482D-964A-CA6D0F308B8E} + {4B2C6EBE-E719-9F40-ADE6-C82DA632E554} = {18791734-CA81-482D-964A-CA6D0F308B8E} + {1ACFAAE8-C86D-4582-B0B4-542B74970737} = {80F3B34B-C334-44D2-A861-31FD403AD57D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {379A56DA-D3F0-4E7E-8FF7-DA8E20015BF3}