dev_lx #5

Merged
LiuXin merged 6 commits from dev_lx into main 4 months ago
  1. 0
      WeiCloud.Fusion/AlarmService/Alarm.Application/Alarm.Application.csproj
  2. 1
      WeiCloud.Fusion/AlarmService/Alarm.Application/RequestDto/SubscribeEventReqDto.cs
  3. 0
      WeiCloud.Fusion/AlarmService/Alarm.Application/ResponeDto/EventEnvelopeDto.cs
  4. 5
      WeiCloud.Fusion/AlarmService/Alarm.DomainService/Alarm.DomainService.csproj
  5. 37
      WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/DahuaGeneralCtlService.cs
  6. 0
      WeiCloud.Fusion/AlarmService/Alarm.DomainService/DahAlarm/IDahuaGeneralCtlService.cs
  7. 5
      WeiCloud.Fusion/AlarmService/AlarmService.API/AlarmService.API.csproj
  8. 15
      WeiCloud.Fusion/AlarmService/AlarmService.API/Program.cs
  9. 24
      WeiCloud.Fusion/AlarmService/AlarmService.API/appsettings.json
  10. 3
      WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/AppHost.cs
  11. 1
      WeiCloud.Fusion/AspireApp/Manage.AppHost.AppHost/Manage.AppHost.AppHost.csproj
  12. 29
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.csproj
  13. 6
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Common.Shared.API.http
  14. 15
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/AutoMapperProfile.cs
  15. 31
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Infrastructure/ConfigureAutofac.cs
  16. 57
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/NLog.config
  17. 123
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Program.cs
  18. 41
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Properties/launchSettings.json
  19. 19
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/Startup.cs
  20. 13
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/WeatherForecast.cs
  21. 17
      WeiCloud.Fusion/Common.SharedService/Common.Shared.API/appsettings.json
  22. 71
      WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/DaHApiResult.cs
  23. 30
      WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/RequestDto/DahuaTokenQueryDto.cs
  24. 18
      WeiCloud.Fusion/Common.SharedService/Common.Shared.Application/DaHua/ResponeDto/DahuaTokenResDto.cs
  25. 1
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/Common.Shared.DomainService.csproj
  26. 13
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/ITokenProviderService.cs
  27. 35
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenCacheService.cs
  28. 349
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/DaHTokenService/TokenProviderService.cs
  29. 5
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/IMqttClientService.cs
  30. 17
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MQTTClient.cs
  31. 2
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientListService.cs
  32. 5
      WeiCloud.Fusion/Common.SharedService/Common.Shared.DomainService/MqttClient/MqttClientService.cs
  33. 24
      WeiCloud.Fusion/VideoService/Video.API/Controllers/DaHua/VideoManageController.cs
  34. 50
      WeiCloud.Fusion/VideoService/Video.API/HostService/DahuaTokenRefreshJob.cs
  35. 28
      WeiCloud.Fusion/VideoService/Video.API/Program.cs
  36. 1
      WeiCloud.Fusion/VideoService/Video.API/Video.API.csproj
  37. 12
      WeiCloud.Fusion/VideoService/Video.API/appsettings.json
  38. 110
      WeiCloud.Fusion/VideoService/Video.Application/RequestDto/DahuaVideoQueryDto.cs
  39. 85
      WeiCloud.Fusion/VideoService/Video.Application/ResponeDto/DahuaVideoResDto.cs
  40. 381
      WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/DahuaGeneralCtlService.cs
  41. 32
      WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IDahuaGeneralCtlService.cs
  42. 11
      WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/IRootVideoPlaybackService.cs
  43. 43
      WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/RootVideoPlaybackService.cs
  44. 19
      WeiCloud.Fusion/VideoService/Video.DomainService/Dahvision/TokenCacheService.cs
  45. 1
      WeiCloud.Fusion/VideoService/Video.DomainService/Video.DomainService.csproj
  46. 35
      WeiCloud.Fusion/WeiCloud.Fusion.sln

@ -7,7 +7,6 @@ namespace Alarm.Application.RequestDto
[JsonPropertyName("param")]
public required SubscribeParam Param { get; init; }
}
public sealed class SubscribeParam
{
[JsonPropertyName("monitors")]

@ -13,9 +13,10 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.Application\Common.Shared.Application.csproj" />
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.DomainService\Common.Shared.DomainService.csproj" />
<ProjectReference Include="..\..\WeiCloud.Core\WeiCloud.Core.csproj" />
<ProjectReference Include="..\Alarm.Application\Alarm.Application.csproj" />
<ProjectReference Include="..\Common.SharedService\Common.Shared.Application\Common.Shared.Application.csproj" />
<ProjectReference Include="..\WeiCloud.Core\WeiCloud.Core.csproj" />
</ItemGroup>
</Project>

@ -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<DahuaGeneralCtlService> _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<DahuaGeneralCtlService> logger, IConfiguration configuration, HttpClient http)
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="configuration"></param>
/// <param name="http"></param>
/// <param name="mQTTClient"></param>
public DahuaGeneralCtlService(ILogger<DahuaGeneralCtlService> 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;
}
/// <summary>
@ -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<object>
{
@ -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);
}
}
}

@ -4,12 +4,15 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Alarm.DomainService\Alarm.DomainService.csproj" />
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.Application\Common.Shared.Application.csproj" />
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.DomainService\Common.Shared.DomainService.csproj" />
<ProjectReference Include="..\..\WeiCloud.Utils\WeiCloud.Utils.csproj" />
<ProjectReference Include="..\Alarm.Application\Alarm.Application.csproj" />
<ProjectReference Include="..\Alarm.DomainService\Alarm.DomainService.csproj" />
</ItemGroup>
<ItemGroup>

@ -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<ITokenProviderService, TokenProviderService>();
#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<MQTTClient>();
builder.Services.AddSingleton<IMqttClientService, MqttClientService>();
#endregion mqttclient
var app = builder.Build();
if (app.Environment.IsDevelopment())

@ -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"
}
}

@ -6,9 +6,12 @@ var apiService = builder.AddProject<Projects.Manage_AppHost_ApiService>("apiserv
var videoapi = builder.AddProject<Projects.Video_API>("videoapi");
var alarmapi = builder.AddProject<Projects.AlarmService_API>("alarmapi");
var sharedapi = builder.AddProject<Projects.Common_Shared_API>("sharedapi");
builder.AddProject<Projects.Manage_AppHost_Web>("webfrontend")
.WithReference(videoapi)
.WithReference(alarmapi)
.WithReference(sharedapi)
.WithExternalHttpEndpoints()
.WithHttpHealthCheck("/health")
.WithReference(apiService)

@ -12,6 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\..\AlarmService\AlarmService.API\AlarmService.API.csproj" />
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.API\Common.Shared.API.csproj" />
<ProjectReference Include="..\..\VideoService\Video.API\Video.API.csproj" />
<ProjectReference Include="..\Manage.AppHost.ApiService\Manage.AppHost.ApiService.csproj" />
<ProjectReference Include="..\Manage.AppHost.Web\Manage.AppHost.Web.csproj" />

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="8.4.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="NLog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.3" />
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WeiCloud.Utils\WeiCloud.Utils.csproj" />
<ProjectReference Include="..\Common.Shared.DomainService\Common.Shared.DomainService.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,6 @@
@Common.Shared.API_HostAddress = http://localhost:5258
GET {{Common.Shared.API_HostAddress}}/weatherforecast/
Accept: application/json
###

@ -0,0 +1,15 @@
namespace Video.API.Infrastructure
{
/// <summary>
/// 配置AutoMapper
/// </summary>
public class AutoMapperProfile
{
/// <summary>
/// 构造函数
/// </summary>
public AutoMapperProfile()
{
}
}
}

@ -0,0 +1,31 @@
using Autofac;
using System.Reflection;
namespace Common.Shared.API.Infrastructure
{
/// <summary>
/// autofac
/// </summary>
public class ConfigureAutofac : Autofac.Module
{
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
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();
}
}
}

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwExceptions="false"
internalLogLevel="Warn" internalLogFile="nlog-internal.log">
<!-- optional, add some variables
https://github.com/nlog/NLog/wiki/Configuration-file#variables
-->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!--<variable name="myvar" value="myvalue"/>-->
<variable name="year" value="${date:format=yyyy}"/>
<variable name="year_month" value="${date:format=yyyy-MM}"/>
<!--
See https://github.com/nlog/nlog/wiki/Configuration-file
for information on customizing logging rules and outputs.
-->
<targets async="true">
<!--
add your targets here
See https://github.com/nlog/NLog/wiki/Targets for possible targets.
See https://github.com/nlog/NLog/wiki/Layout-Renderers for the possible layout renderers.
-->
<!--
Write events to a file with the date in the filename.
<target xsi:type="File" name="f" fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
-->
<target xsi:type="AsyncWrapper" name="MyLogger">
<target xsi:type="File"
layout="${longdate},${uppercase:${level}},${message}"
fileName="${basedir}/Log/${level}/${year}/${year_month}/${shortdate}.log" encoding="utf-8" />
</target>
<target xsi:type="Null" name="blackhole" />
</targets>
<rules>
<!-- add your logging rules here -->
<!--
Write all events with minimal level of Debug (So Debug, Info, Warn, Error and Fatal, but not Trace) to "f"
<logger name="*" minlevel="Debug" writeTo="f" />
-->
<!--跳过Microsoft的系统日志-->
<logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" />
<logger name="*" minlevel="Debug" writeTo="MyLogger" final="true"/>
<logger name="Microsoft.*" minlevel="Warn" writeTo="MyLogger" final="true"/>
</rules>
</nlog>

@ -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<AddProjectHeaderParameter>();
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<ContainerBuilder>(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();
}
}
}
}

@ -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"
}
}
}
}

@ -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;
}
}
}

@ -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; }
}
}

@ -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": "*"
}

@ -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<T>
{
[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<string?>
{
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<T>
{
[JsonPropertyName("code")]
public int Code { get; set; } // 修改为 int 类型
[JsonPropertyName("desc")] // 修改为 "desc" 而不是 "errMsg"
public string Desc { get; set; }
[JsonPropertyName("data")]
public T Data { get; set; }

@ -59,4 +59,34 @@ namespace Common.Shared.Application.DaHua
[JsonPropertyName("verifyCodeFlag")]
public int VerifyCodeFlag { get; set; } = 0;
}
/// <summary>
/// 刷新 access_token 的请求参数模型
/// </summary>
public class RefreshTokenReqDto
{
/// <summary>
/// 认证类型,固定值:refresh_token
/// </summary>
[JsonPropertyName("grant_type")]
public string GrantType { get; set; } = "refresh_token"; // 默认值,通常固定
/// <summary>
/// 客户端ID(与认证接口中一致)
/// </summary>
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
/// <summary>
/// 客户端密钥(与认证接口中一致)
/// </summary>
[JsonPropertyName("client_secret")]
public string ClientSecret { get; set; }
/// <summary>
/// 刷新令牌(refresh_token),用于获取新的 access_token
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
}
}

@ -35,13 +35,13 @@ namespace Common.Shared.Application.DaHua
public string ClientId { get; set; }
/// <summary>
/// 授权范围,固定为 ["*"]
/// 授权范围
/// </summary>
[JsonPropertyName("scope")]
public string[] Scope { get; set; }
public string Scope { get; set; } // 改为string类型
/// <summary>
/// access_token 有效期(秒),默认 2 小时(7200 秒)
/// access_token 有效期(秒)
/// </summary>
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
@ -50,19 +50,25 @@ namespace Common.Shared.Application.DaHua
/// 鉴权 Token
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
public string? AccessToken { get; set; }
/// <summary>
/// 刷新 Token(有效期 1 天)
/// 刷新 Token
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// Token 类型,固定为 "bearer"
/// Token 类型
/// </summary>
[JsonPropertyName("token_type")]
public string TokenType { get; set; } = "bearer";
/// <summary>
/// 剩余天数(新增字段)
/// </summary>
[JsonPropertyName("remainderDays")]
public int RemainderDays { get; set; }
}
/// <summary>

@ -9,7 +9,6 @@
<ItemGroup>
<PackageReference Include="MQTTnet" Version="4.1.4.563" />
<PackageReference Include="NLog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.3" />
</ItemGroup>
<ItemGroup>

@ -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<string> GetTokenAsync(string clientId);
}
}

@ -0,0 +1,35 @@
using System.Collections.Concurrent;
namespace Common.Shared.DomainService
{
public static class TokenCache
{
public static readonly ConcurrentDictionary<string, TokenEntry> TokenMap = new();
}
public static class TokenLockProvider
{
private static readonly ConcurrentDictionary<string, SemaphoreSlim> LockMap = new();
public static SemaphoreSlim GetLock(string key)
{
return LockMap.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
}
}
/// <summary>
/// 用于token缓存的条目类
/// </summary>
public class TokenEntry
{
/// <summary>
/// token:这是token_type + 空格 + access_token这样格式的
/// </summary>
public string? AccessToken { get; set; }
/// <summary>
/// 添加时间
/// </summary>
public DateTimeOffset ExpireAt { get; set; }
}
}

@ -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
{
/// <summary>
/// 获取大华icc平台的token服务
/// </summary>
public class TokenProviderService : ITokenProviderService
{
private readonly IConfiguration _configuration;
private readonly ILogger<TokenProviderService> _logger;
public TokenProviderService(IConfiguration configuration, ILogger<TokenProviderService> logger)
{
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// 开发测试的时候,忽略证书
/// </summary>
private static readonly HttpClient _http = new(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
public async Task<string> 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<TokenEntry> 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<TokenEntry> GetDaHToken()
{
//1. 获取公钥
DaHApiResult<PublicKeyDto> 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<LoginResDto> loginResult = await GetToken(dto);
TokenEntry refreshed = new()
{
AccessToken = loginResult.Data.AccessToken,
ExpireAt = DateTimeOffset.UtcNow.AddSeconds(120)
};
return refreshed;
}
/// <summary>
/// 刷新token,2个小时过期的
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
private async Task<DaHApiResult<TokenResDto>> RefreshToken(RefreshTokenReqDto dto)
{
DaHApiResult<TokenResDto> result = new DaHApiResult<TokenResDto>() { 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<DaHApiResult<TokenResDto>>();
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;
}
/// <summary>
/// 获取公钥
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private async Task<DaHApiResult<PublicKeyDto>> GetPublicKey()
{
DaHApiResult<PublicKeyDto> 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<DaHApiResult<PublicKeyDto>>(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;
}
/// <summary>
/// 获取token
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
private async Task<DaHApiResult<LoginResDto>> GetToken(LoginRequestDto dto)
{
DaHApiResult<LoginResDto> 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<DaHApiResult<LoginResDto>>();
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("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
Console.WriteLine(xmlpublicKey);
return xmlpublicKey;
}
#endregion RES加密
}
}

@ -1,7 +1,6 @@
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client;
namespace Common.Shared.DomainService.MqttClient
namespace Common.Shared.DomainService
{
public interface IMqttClientService
{

@ -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<MQTTClient> _logger;
internal IMqttClient mqttClient;
public bool IsConnected => mqttClient != null && mqttClient.IsConnected;
public MQTTClient(ILogger<MQTTClient> logger)
{
_logger = logger;
}
/// <summary>
/// 如果未连接,则按参数调用 Init;成功后返回当前连接状态。
/// </summary>
public async Task<bool> 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))

@ -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
{

@ -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
{

@ -6,6 +6,9 @@ using WeiCloud.Core.BaseModels;
namespace Video.API.Controllers.DaHua
{
/// <summary>
/// 大华视频
/// </summary>
[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;
/// <summary>
/// 构造
/// </summary>
/// <param name="logger"></param>
/// <param name="rootVideoPlaybackService"></param>
/// <param name="configuration"></param>
public VideoManageController(ILogger<VideoManageController> logger, IRootVideoPlaybackService rootVideoPlaybackService, IConfiguration configuration)
{
_logger = logger;
@ -23,17 +32,6 @@ namespace Video.API.Controllers.DaHua
#region 大华视频处理
/// <summary>
/// 大华视频的登录获取Token
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("token/dh")]
public async Task<ApiResult<string>> GetDaHToken(LoginRequestDto dto)
{
return await _rootVideoPlaybackService.GetDaHToken(dto);
}
/// <summary>
/// 大华视频回放
/// </summary>
@ -51,7 +49,7 @@ namespace Video.API.Controllers.DaHua
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("rtspplayback/dh")]
public async Task<ApiResult<UrlDataDto>> RtspPlaybackByTime(PlaybackReqDto dto)
public async Task<ApiResult<UrlDataDto>> RtspPlaybackByTime(RtspPlaybackReqDto dto)
{
return await _rootVideoPlaybackService.RtspPlaybackByTime(dto);
}
@ -73,7 +71,7 @@ namespace Video.API.Controllers.DaHua
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("rtspstart/dh")]
public async Task<ApiResult<UrlDataDto>> RtspStartVideoUrl(StreamReqDto dto)
public async Task<ApiResult<UrlDataDto>> RtspStartVideoUrl(StreamRtspReqDto dto)
{
return await _rootVideoPlaybackService.RtspStartVideoUrl(dto);
}

@ -1,50 +0,0 @@
using Quartz;
using Video.DomainService;
namespace Video.API.HostService
{
/// <summary>
/// 大华的token的刷新任务
/// </summary>
public class DahuaTokenRefreshJob : IJob
{
private readonly ILogger<DahuaTokenRefreshJob> _logger;
private readonly IDahuaGeneralCtlService _dahuaGeneralCtlService;
private readonly IConfiguration _configuration;
public DahuaTokenRefreshJob(IDahuaGeneralCtlService dahuaGeneralCtlService, ILogger<DahuaTokenRefreshJob> 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);
}
}
}
}
}

@ -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<MicrosoftDependencyInjectionJobFactory>();
//新增的 DahuaToken 刷新任务
var jobKey2 = new JobKey("DahuaTokenRefresh");
q.AddJob<DahuaTokenRefreshJob>(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<ITokenProviderService, TokenProviderService>();
#endregion 注册大华token 服务
var app = builder.Build();
if (app.Environment.IsDevelopment())

@ -9,7 +9,6 @@
<ItemGroup>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="AutoMapper" Version="15.0.1" />
<PackageReference Include="DotNetCore.CAP.Dashboard" Version="8.3.5" />
<PackageReference Include="DotNetCore.CAP.MySql" Version="8.3.5" />
<PackageReference Include="DotNetCore.CAP.RedisStreams" Version="8.3.5" />

@ -25,10 +25,12 @@
"VideoOpen": "1", //01
//
"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"
}
}

@ -6,39 +6,57 @@ using System.Text.Json.Serialization;
namespace Video.Application
{
/// <summary>
/// 刷新 access_token 的请求参数模型
/// hls、rtmp回放请求的数据部分
/// </summary>
public class RefreshTokenReqDto
public class PlaybackReqDto
{
/// <summary>
/// 认证类型,固定值:refresh_token
/// 通道ID(格式如:1000018$1$0$0)
/// </summary>
[JsonPropertyName("grant_type")]
public string GrantType { get; set; } = "refresh_token"; // 默认值,通常固定
[JsonPropertyName("channelId")]
public string ChannelId { get; set; }
/// <summary>
/// 客户端ID(与认证接口中一致)
/// 流类型:1-主码流,2-子码流(通常为字符串或数字)
/// </summary>
[JsonPropertyName("streamType")]
public string StreamType { get; set; }
/// <summary>
/// 输出类型,如 "hls"、"rtmp" 等,如果RTSP的回放,不加此字段
/// </summary>
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; } = "hls";
/// <summary>
/// 客户端密钥(与认证接口中一致)
/// 录像类型:1-定时录像,2-移动侦测,3-报警录像等(字符串形式
/// </summary>
[JsonPropertyName("client_secret")]
public string ClientSecret { get; set; }
[JsonPropertyName("recordType")]
public string RecordType { get; set; }
/// <summary>
/// 刷新令牌(refresh_token),用于获取新的 access_token
/// 回放开始时间,格式:"yyyy-M-d HH:mm:ss"
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("beginTime")]
public string BeginTime { get; set; }
/// <summary>
/// 回放结束时间,格式:"yyyy-M-d HH:mm:ss"
/// </summary>
[JsonPropertyName("endTime")]
public string EndTime { get; set; }
/// <summary>
/// 录像来源:1-设备,2-平台,3-云端等
/// </summary>
[JsonPropertyName("recordSource")]
public string RecordSource { get; set; }
}
/// <summary>
/// 回放请求的数据部分
/// hls、rtmp回放请求的数据部分
/// </summary>
public class PlaybackReqDto
public class RtspPlaybackReqDto
{
/// <summary>
/// 通道ID(格式如:1000018$1$0$0)
@ -52,12 +70,6 @@ namespace Video.Application
[JsonPropertyName("streamType")]
public string StreamType { get; set; }
/// <summary>
/// 输出类型,如 "hls"、"rtmp" 等,如果RTSP的回放,不加此字段
/// </summary>
[JsonPropertyName("type")]
public string? Type { get; set; } = "hls";
/// <summary>
/// 录像类型:1-定时录像,2-移动侦测,3-报警录像等(字符串形式)
/// </summary>
@ -67,8 +79,8 @@ namespace Video.Application
/// <summary>
/// 回放开始时间,格式:"yyyy-M-d HH:mm:ss"
/// </summary>
[JsonPropertyName("beginTime")]
public string BeginTime { get; set; }
[JsonPropertyName("startTime")]
public string startTime { get; set; }
/// <summary>
/// 回放结束时间,格式:"yyyy-M-d HH:mm:ss"
@ -81,11 +93,6 @@ namespace Video.Application
/// </summary>
[JsonPropertyName("recordSource")]
public string RecordSource { get; set; }
/// <summary>
/// 鉴权的token
/// </summary>
public string? Token { get; set; }
}
/// <summary>
@ -223,29 +230,54 @@ namespace Video.Application
/// </summary>
[JsonPropertyName("type")]
public string? Type { get; set; }
}
/// <summary>
/// 实时 流播放请求响应包装类
/// </summary>
public class StreamReqDto
{
/// <summary>
/// rtsp专用,有datatype没有type,有type没有datatype
/// </summary>
[JsonPropertyName("dataType")]
public string? DataType { get; set; }
/// <summary>
/// 请求头认证
/// 请求数据
/// </summary>
public string? Token { get; set; }
[JsonPropertyName("data")]
public StreamRequestData Data { get; set; }
}
/// <summary>
/// 实时 流播放请求响应包装类
/// </summary>
public class StreamReqDto
public class StreamRtspReqDto
{
/// <summary>
/// 请求数据
/// </summary>
[JsonPropertyName("data")]
public StreamRequestData Data { get; set; }
public StreamRtspRequestData Data { get; set; }
}
/// <summary>
/// 实时流播放请求数据实体
/// </summary>
public class StreamRtspRequestData
{
/// <summary>
/// 通道编码
/// </summary>
[JsonPropertyName("channelId")]
public string ChannelId { get; set; }
/// <summary>
/// 码流类型:1-主码流,2-子码流
/// </summary>
[JsonPropertyName("streamType")]
public string StreamType { get; set; }
/// <summary>
/// rtsp专用,有datatype没有type,有type没有datatype
/// </summary>
[JsonPropertyName("dataType")]
public string? DataType { get; set; }
}
/// <summary>

@ -305,7 +305,92 @@ namespace Video.Application
/// </summary>
public class UrlDataDto
{
/// <summary>
/// 最小速率
/// </summary>
[JsonPropertyName("minRate")]
public object MinRate { get; set; } // 用object类型兼容null和可能的数值类型
/// <summary>
/// 协议类型
/// </summary>
[JsonPropertyName("protocol")]
public string Protocol { get; set; } // 可为null
/// <summary>
/// IP地址
/// </summary>
[JsonPropertyName("ip")]
public string Ip { get; set; } // 可为null
/// <summary>
/// 端口号
/// </summary>
[JsonPropertyName("port")]
public object Port { get; set; } // 用object类型兼容null和可能的数值类型
/// <summary>
/// STUN启用状态
/// </summary>
[JsonPropertyName("stunEnable")]
public bool? StunEnable { get; set; } // 可空布尔类型
/// <summary>
/// STUN端口
/// </summary>
[JsonPropertyName("stunPort")]
public object StunPort { get; set; } // 用object类型兼容null和可能的数值类型
/// <summary>
/// RTSP地址
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; }
/// <summary>
/// 连接类型
/// </summary>
[JsonPropertyName("connectType")]
public string ConnectType { get; set; } // 可为null
/// <summary>
/// 会话标识
/// </summary>
[JsonPropertyName("session")]
public string Session { get; set; }
/// <summary>
/// 令牌
/// </summary>
[JsonPropertyName("token")]
public string Token { get; set; }
/// <summary>
/// 轨道标识
/// </summary>
[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; } // 网络标识
}
}

@ -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<DahuaGeneralCtlService> _logger;
private readonly IConfiguration _configuration;
private readonly HttpClient _http;
private readonly ITokenProviderService _tokenProviderService;
// private readonly HttpClient _http;
public DahuaGeneralCtlService(ILogger<DahuaGeneralCtlService> logger, IConfiguration configuration, HttpClient http)
public DahuaGeneralCtlService(ILogger<DahuaGeneralCtlService> logger, IConfiguration configuration, ITokenProviderService tokenProviderService)
{
_logger = logger;
_configuration = configuration;
_http = http;
}
/// <summary>
/// 获取公钥
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<DaHApiResult<PublicKeyDto>> GetPublicKey()
{
DaHApiResult<PublicKeyDto> 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<DaHApiResult<PublicKeyDto>>(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;
}
/// <summary>
/// 获取token
/// 开发测试的时候,忽略证书
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<DaHApiResult<LoginResDto>> GetToken(LoginRequestDto dto)
private static readonly HttpClient _http = new HttpClient(new HttpClientHandler
{
DaHApiResult<LoginResDto> 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<DaHApiResult<LoginResDto>>();
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
});
/// <summary>
/// 录像回放
@ -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
}
}
/// <summary>
/// 刷新token,2个小时过期的
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public async Task<DaHApiResult<TokenResDto>> RefreshToken(RefreshTokenReqDto dto)
{
DaHApiResult<TokenResDto> result = new DaHApiResult<TokenResDto>() { 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<DaHApiResult<TokenResDto>>();
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;
}
/// <summary>
/// 设备通道分页查询
/// </summary>
@ -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
}
}
/// <summary>
/// 获取AccessToken
/// </summary>
/// <param name="clientId"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<string> 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; // 返回空字符串表示获取失败
}
}
/// <summary>
/// rtsp录像回放
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<DaHApiResult<UrlDataDto>> RtspPlaybackByTime(PlaybackReqDto dto)
public async Task<DaHApiResult<UrlDataDto>> 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
/// <param name="dto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<DaHApiResult<UrlDataDto>> RtspStartVideoUrl(StreamReqDto dto)
public async Task<DaHApiResult<UrlDataDto>> 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<DaHApiResult<UrlDataDto>>(body);
if (result == null || !result.Success || result.Code != "0")
if (result == null || !result.Success || result.Code != "100")
{
_logger.LogWarning("实时流请求业务失败: {Body}", body);
return new DaHApiResult<UrlDataDto> { Success = false, Code = "1010", Msg = "实时流请求失败" };
@ -579,171 +404,5 @@ namespace Video.DomainService
return new DaHApiResult<UrlDataDto> { Success = false, Code = "1010", Msg = "实时流请求失败" };
}
}
#region GetToken的辅助方法
private async Task<string> 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<TokenEntry> 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("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
Console.WriteLine(xmlpublicKey);
return xmlpublicKey;
}
#endregion RES加密
}
}

@ -8,26 +8,6 @@ namespace Video.DomainService
/// </summary>
public interface IDahuaGeneralCtlService
{
/// <summary>
/// 获取公钥
/// </summary>
/// <returns></returns>
Task<DaHApiResult<PublicKeyDto>> GetPublicKey();
/// <summary>
/// 鉴权
/// </summary>
/// <param name="pKey">PublicKey</param>
/// <returns></returns>
Task<DaHApiResult<LoginResDto>> GetToken(LoginRequestDto dto);
/// <summary>
/// 刷新token
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<DaHApiResult<TokenResDto>> RefreshToken(RefreshTokenReqDto dto);
/// <summary>
/// 查询普通录像信息列表(后续可能用于hls的拼接)
/// </summary>
@ -47,7 +27,7 @@ namespace Video.DomainService
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<DaHApiResult<UrlDataDto>> RtspPlaybackByTime(PlaybackReqDto dto);
Task<DaHApiResult<UrlDataDto>> RtspPlaybackByTime(RtspPlaybackReqDto dto);
/// <summary>
/// 设备通道分页查询,需要用于HlsRecordVideo
@ -69,20 +49,12 @@ namespace Video.DomainService
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<DaHApiResult<UrlDataDto>> RtspStartVideoUrl(StreamReqDto dto);
Task<DaHApiResult<UrlDataDto>> RtspStartVideoUrl(StreamRtspReqDto dto);
/// <summary>
/// 注销认证信息
/// 没有返回值
/// </summary>
Task<DaHApiResult<object>> Logout(string authorization, string? openId, int? userClient);
/// <summary>
/// 根据 clientId 获取当前可用 token(自动处理过期)
/// </summary>
/// <param name="clientId">如果不传就从appsetting中得到</param>
/// <param name="ct"></param>
/// <returns></returns>
Task<string> GetAccessTokenAsync(string? clientId, CancellationToken ct = default);
}
}

@ -13,13 +13,6 @@ namespace Video.DomainService
/// <returns></returns>
Task<ApiResult<UrlDataDto>> GetDaHRecordVideoUrl(PlaybackReqDto dto);
/// <summary>
/// 大华的token获取
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<ApiResult<string>> GetDaHToken(LoginRequestDto dto);
/// <summary>
/// 大华的实时视频
/// </summary>
@ -32,14 +25,14 @@ namespace Video.DomainService
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<ApiResult<UrlDataDto>> RtspStartVideoUrl(StreamReqDto dto);
Task<ApiResult<UrlDataDto>> RtspStartVideoUrl(StreamRtspReqDto dto);
/// <summary>
/// rtsp录像回放
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<ApiResult<UrlDataDto>> RtspPlaybackByTime(PlaybackReqDto dto);
Task<ApiResult<UrlDataDto>> RtspPlaybackByTime(RtspPlaybackReqDto dto);
/// <summary>
/// 大华设备通道分页查询

@ -50,45 +50,6 @@ namespace Video.DomainService
return result;
}
/// <summary>
/// 大华的鉴权
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<ApiResult<string>> GetDaHToken(LoginRequestDto dto)
{
ApiResult<string> result = new() { Code = 200, Msg = "接口调用成功" };
//1. 获取公钥
DaHApiResult<PublicKeyDto> 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<LoginResDto> 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;
}
/// <summary>
/// 大华实时
/// </summary>
@ -160,7 +121,7 @@ namespace Video.DomainService
/// <param name="dto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<ApiResult<UrlDataDto>> RtspStartVideoUrl(StreamReqDto dto)
public async Task<ApiResult<UrlDataDto>> RtspStartVideoUrl(StreamRtspReqDto dto)
{
ApiResult<UrlDataDto> result = new ApiResult<UrlDataDto>() { Code = 200, Msg = "接口调用成功" };
var urlReult = await _dahuaGeneralCtlService.RtspStartVideoUrl(dto);
@ -179,7 +140,7 @@ namespace Video.DomainService
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<ApiResult<UrlDataDto>> RtspPlaybackByTime(PlaybackReqDto dto)
public async Task<ApiResult<UrlDataDto>> RtspPlaybackByTime(RtspPlaybackReqDto dto)
{
ApiResult<UrlDataDto> result = new ApiResult<UrlDataDto>() { Code = 200, Msg = "接口调用成功" };

@ -1,19 +0,0 @@
using System.Collections.Concurrent;
namespace Video.DomainService
{
public static class TokenCache
{
public static readonly ConcurrentDictionary<string, TokenEntry> TokenMap = new();
}
/// <summary>
/// 用于token缓存的条目类
/// </summary>
public class TokenEntry
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTimeOffset ExpireAt { get; set; }
}
}

@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.Application\Common.Shared.Application.csproj" />
<ProjectReference Include="..\..\Common.SharedService\Common.Shared.DomainService\Common.Shared.DomainService.csproj" />
<ProjectReference Include="..\..\WeiCloud.Core\WeiCloud.Core.csproj" />
<ProjectReference Include="..\..\WeiCloud.Utils\WeiCloud.Utils.csproj" />
<ProjectReference Include="..\Video.Application\Video.Application.csproj" />

@ -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}

Loading…
Cancel
Save