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