396 lines
17 KiB
C#
396 lines
17 KiB
C#
|
|
using System.Text.Json;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraDashboard
|
|||
|
|
{
|
|||
|
|
public partial class CameraRepository
|
|||
|
|
{
|
|||
|
|
#region GetCameraDetailsAsync 取单个摄像头详情
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取摄像头的详细配置信息。
|
|||
|
|
/// 此方法封装了完整的 API 调用和 JSON 到 DTO 的手动映射过程。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="cameraId">要查询的摄像头的唯一 ID。</param>
|
|||
|
|
/// <returns>
|
|||
|
|
/// 一个异步任务,其结果是一个 <see cref="CameraEditInfo"/> 对象,包含摄像头的详细配置。
|
|||
|
|
/// 如果配置节点信息不存在、API 请求失败或 JSON 解析失败,则返回 <c>null</c>。
|
|||
|
|
/// </returns>
|
|||
|
|
public async Task<CameraEditInfo?> GetCameraDetailsAsync(long cameraId)
|
|||
|
|
{
|
|||
|
|
// 1. 从全局数据中获取当前使用的服务节点信息
|
|||
|
|
var serviceNode = AppGlobal.UseServiceNode;
|
|||
|
|
if (serviceNode == null)
|
|||
|
|
{
|
|||
|
|
// 如果没有配置服务节点,则无法获取信息
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var ipAddress = serviceNode.ServiceNodeIp;
|
|||
|
|
var port = serviceNode.ServiceNodePort;
|
|||
|
|
|
|||
|
|
// 2. 拼接 API 请求的 URL
|
|||
|
|
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras/{cameraId}";
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 3. 调用 WebApiService 发送 GET 请求,获取原始 JSON 字符串
|
|||
|
|
string jsonResponse = await WebApiService.Instance.GetAsync(requestUrl, "GetDetail");
|
|||
|
|
|
|||
|
|
// 4. 检查返回的 JSON 是否为空
|
|||
|
|
if (string.IsNullOrEmpty(jsonResponse))
|
|||
|
|
{
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 将原始 JSON 字符串手动映射到目标 DTO 对象
|
|||
|
|
// 这种方式可以精确控制每个字段的转换,处理类型不匹配等问题。
|
|||
|
|
return MapJsonToEditDto(jsonResponse);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
// 6. 捕获所有可能的异常(网络错误、服务器错误等)
|
|||
|
|
System.Diagnostics.Debug.WriteLine($"[Repository] 获取摄像头详情失败 (ID: {cameraId}): {ex.Message}");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region UpdateCameraAsync 更新单个摄像头
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// [新增] 更新摄像头的配置信息。
|
|||
|
|
/// 此方法负责将 <see cref="CameraEditInfo"/> 对象转换为 API 所需的 JSON 格式,并发送 PUT 请求。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="dto">包含摄像头新配置信息的 DTO 对象。</param>
|
|||
|
|
/// <returns>
|
|||
|
|
/// 一个异步任务,其结果为一个布尔值:
|
|||
|
|
/// - <c>true</c>:表示更新成功(HTTP 响应为 200 OK 或其他成功状态码)。
|
|||
|
|
/// - <c>false</c>:表示更新失败(如 DTO 为 null、网络错误、服务器返回错误等)。
|
|||
|
|
/// </returns>
|
|||
|
|
public async Task<bool> UpdateCameraAsync(CameraEditInfo dto, string pageName)
|
|||
|
|
{
|
|||
|
|
// 1. 防御性检查:确保传入的 DTO 对象不为 null
|
|||
|
|
if (dto == null)
|
|||
|
|
{
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. 从全局数据中获取当前使用的服务节点信息
|
|||
|
|
var serviceNode = AppGlobal.UseServiceNode;
|
|||
|
|
if (serviceNode == null)
|
|||
|
|
{
|
|||
|
|
// 如果没有配置服务节点,则无法获取信息
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var ipAddress = serviceNode.ServiceNodeIp;
|
|||
|
|
var port = serviceNode.ServiceNodePort;
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 2. 拼接 PUT 请求的 URL
|
|||
|
|
// URL 格式: http://{ip}:{port}/api/Cameras/{id}
|
|||
|
|
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras/{dto.Id}";
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 3. 将 DTO 对象手动映射为 API 所需的 JSON 字符串
|
|||
|
|
// 这一步至关重要,因为它处理了特殊的字段转换,例如:
|
|||
|
|
// 将 DTO 中的 `Brand` (int) 转换为 JSON 中的 `"brand": "HikVision"` (string)。
|
|||
|
|
string jsonPayload = MapDtoToEditJson(dto);
|
|||
|
|
|
|||
|
|
// 4. 调用 WebApiService 发送 PUT 请求
|
|||
|
|
// 注意:请确保您的 `WebApiService` 类中存在 `PutAsync` 方法。
|
|||
|
|
// 如果不存在,可以考虑添加一个,或者在某些 RESTful 设计中,也可以使用 `PostAsync` 代替。
|
|||
|
|
await WebApiService.Instance.PutAsync(requestUrl, jsonPayload, pageName);
|
|||
|
|
|
|||
|
|
// 5. 如果代码执行到这里,说明请求成功且没有抛出异常
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
// 6. 捕获所有可能的异常(网络错误、服务器错误、序列化错误等)
|
|||
|
|
System.Diagnostics.Debug.WriteLine($"[Repository] 更新摄像头配置失败 (ID: {dto.Id}): {ex.Message}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region CreateCameraAsync 新增单个摄像头
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// [新增] 创建一个新的摄像头配置。
|
|||
|
|
/// 此方法负责将 <see cref="CameraEditInfo"/> 对象转换为 API 所需的 JSON 格式,并发送 POST 请求到摄像头资源集合的根路径。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="nodeIp">目标服务器节点的 IP 地址。</param>
|
|||
|
|
/// <param name="nodePort">目标服务器节点的端口号。</param>
|
|||
|
|
/// <param name="dto">包含新摄像头配置信息的 DTO 对象。</param>
|
|||
|
|
/// <returns>
|
|||
|
|
/// 一个异步任务,其结果为一个布尔值:
|
|||
|
|
/// - <c>true</c>:表示创建成功(HTTP 响应为 201 Created 或其他成功状态码)。
|
|||
|
|
/// - <c>false</c>:表示创建失败(如 DTO 为 null、网络错误、服务器返回错误等)。
|
|||
|
|
/// </returns>
|
|||
|
|
public async Task<bool> CreateCameraAsync(CameraEditInfo dto, string pageName)
|
|||
|
|
{
|
|||
|
|
// 1. 防御性检查:确保传入的 DTO 对象不为 null
|
|||
|
|
if (dto == null)
|
|||
|
|
{
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. 从全局数据中获取当前使用的服务节点信息
|
|||
|
|
var serviceNode = AppGlobal.UseServiceNode;
|
|||
|
|
if (serviceNode == null)
|
|||
|
|
{
|
|||
|
|
// 如果没有配置服务节点,则无法获取信息
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var ipAddress = serviceNode.ServiceNodeIp;
|
|||
|
|
var port = serviceNode.ServiceNodePort;
|
|||
|
|
|
|||
|
|
// 2. 拼接 POST 请求的 URL
|
|||
|
|
// URL 格式: http://{ip}:{port}/api/Cameras
|
|||
|
|
// 注意:创建新资源通常是 POST 到资源集合的根路径,而不是单个资源的路径。
|
|||
|
|
// 此时 DTO 中的 `Id` 字段通常应为 0 或默认值,由服务器在创建时生成新的唯一 ID。
|
|||
|
|
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras";
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 3. 将 DTO 对象手动映射为 API 所需的 JSON 字符串
|
|||
|
|
// 复用与更新操作相同的映射逻辑,确保数据格式的一致性。
|
|||
|
|
string jsonPayload = MapDtoToEditJson(dto);
|
|||
|
|
|
|||
|
|
// 4. 调用 WebApiService 发送 POST 请求
|
|||
|
|
await WebApiService.Instance.PostAsync(requestUrl, jsonPayload, pageName);
|
|||
|
|
|
|||
|
|
// 5. 如果代码执行到这里,说明请求成功且没有抛出异常
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
// 6. 捕获所有可能的异常(网络错误、服务器错误、序列化错误等)
|
|||
|
|
System.Diagnostics.Debug.WriteLine($"[Repository] 创建新摄像头失败: {ex.Message}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// [新增] 删除摄像头
|
|||
|
|
/// </summary>
|
|||
|
|
public async Task<bool> DeleteCameraAsync(long cameraId)
|
|||
|
|
{
|
|||
|
|
// 1. 从全局数据中获取当前使用的服务节点信息
|
|||
|
|
var serviceNode = AppGlobal.UseServiceNode;
|
|||
|
|
if (serviceNode == null)
|
|||
|
|
{
|
|||
|
|
// 如果没有配置服务节点,则无法获取信息
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var ipAddress = serviceNode.ServiceNodeIp;
|
|||
|
|
var port = serviceNode.ServiceNodePort;
|
|||
|
|
|
|||
|
|
// URL: http://{ip}:{port}/api/Cameras/{id}
|
|||
|
|
string url = $"http://{ipAddress}:{port}/api/Cameras/{cameraId}";
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 发送 DELETE 请求
|
|||
|
|
await WebApiService.Instance.DeleteAsync(url, "DeleteCamera");
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
System.Diagnostics.Debug.WriteLine($"[Repository] 删除失败: {ex.Message}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region MapJsonToEditDto
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 私有辅助方法:手动解析 JSON 字符串并映射到 <see cref="CameraEditInfo"/> DTO。
|
|||
|
|
/// 此方法能精确处理类型转换和提供默认值,避免自动反序列化时因类型不匹配而失败。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="json">从 API 获取的原始 JSON 字符串。</param>
|
|||
|
|
/// <returns>一个填充了数据的 <see cref="CameraEditInfo"/> 对象。即使解析失败,也会返回一个对象实例。</returns>
|
|||
|
|
private CameraEditInfo MapJsonToEditDto(string json)
|
|||
|
|
{
|
|||
|
|
// 初始化一个 DTO 对象,用于存储映射后的数据
|
|||
|
|
var cameraDto = new CameraEditInfo();
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 使用 System.Text.Json 的 JsonDocument 进行高性能的只读解析
|
|||
|
|
using (JsonDocument doc = JsonDocument.Parse(json))
|
|||
|
|
{
|
|||
|
|
JsonElement root = doc.RootElement;
|
|||
|
|
|
|||
|
|
// --- 基础字段映射 ---
|
|||
|
|
// 使用 TryGetProperty 安全地获取属性,避免因字段不存在而抛出异常
|
|||
|
|
if (root.TryGetProperty("id", out var idElement)) cameraDto.Id = idElement.GetInt64();
|
|||
|
|
if (root.TryGetProperty("name", out var nameElement)) cameraDto.Name = nameElement.GetString() ?? string.Empty;
|
|||
|
|
if (root.TryGetProperty("ipAddress", out var ipElement)) cameraDto.IpAddress = ipElement.GetString();
|
|||
|
|
if (root.TryGetProperty("username", out var userElement)) cameraDto.Username = userElement.GetString();
|
|||
|
|
if (root.TryGetProperty("password", out var passElement)) cameraDto.Password = passElement.GetString();
|
|||
|
|
if (root.TryGetProperty("channelIndex", out var chElement)) cameraDto.ChannelIndex = chElement.GetInt32();
|
|||
|
|
if (root.TryGetProperty("rtspPath", out var rtspElement)) cameraDto.RtspPath = rtspElement.GetString();
|
|||
|
|
if (root.TryGetProperty("mainboardIp", out var mainIpElement)) cameraDto.MainboardIp = mainIpElement.GetString();
|
|||
|
|
if (root.TryGetProperty("mainboardPort", out var mainPortElement)) cameraDto.MainboardPort = mainPortElement.GetInt32();
|
|||
|
|
if (root.TryGetProperty("streamType", out var streamElement)) cameraDto.StreamType = streamElement.GetInt32();
|
|||
|
|
|
|||
|
|
// --- 类型转换 ---
|
|||
|
|
// 将 JSON 中的 int 类型端口号转换为 DTO 中的 ushort 类型
|
|||
|
|
if (root.TryGetProperty("port", out var portElement)) cameraDto.Port = (ushort)portElement.GetInt32();
|
|||
|
|
|
|||
|
|
// --- 特殊逻辑处理 ---
|
|||
|
|
// 将 JSON 中的品牌字符串(如 "HikVision")转换为 DTO 中的整数枚举
|
|||
|
|
if (root.TryGetProperty("brand", out var brandElement))
|
|||
|
|
{
|
|||
|
|
cameraDto.Brand = ParseBrandToEditInt(brandElement.GetString());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- 设置默认值 ---
|
|||
|
|
// 为未从 API 获取或有特定默认值的字段设置初始值
|
|||
|
|
cameraDto.Location = string.Empty;
|
|||
|
|
cameraDto.UseGrayscale = false;
|
|||
|
|
cameraDto.EnhanceImage = true;
|
|||
|
|
cameraDto.AllowCompress = true;
|
|||
|
|
cameraDto.TargetResolution = string.Empty;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
// 捕获 JSON 解析或映射过程中的任何异常
|
|||
|
|
System.Diagnostics.Debug.WriteLine($"[Repository] JSON 解析或映射到 DTO 时发生异常: {ex.Message}");
|
|||
|
|
// 即使发生异常,也返回一个对象实例,而不是 null,以防止上层代码出现空引用异常
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return cameraDto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region ParseBrandToEditInt
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// [读] API 字符串 -> DTO 枚举值 (int)
|
|||
|
|
/// 将 WebAPI 返回的 "HikVision" 等字符串解析为 DeviceBrand 枚举对应的 int
|
|||
|
|
/// </summary>
|
|||
|
|
private int ParseBrandToEditInt(string brandApiString)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(brandApiString))
|
|||
|
|
return (int)DeviceBrand.Unknown;
|
|||
|
|
|
|||
|
|
// 统一转小写进行匹配,防止大小写差异
|
|||
|
|
var lowerName = brandApiString.Trim().ToLower();
|
|||
|
|
|
|||
|
|
// 1. 尝试直接通过枚举名解析 (例如 API 返回 "HikVision", 枚举也是 HikVision)
|
|||
|
|
// 这样如果 API 返回 "Usb",且枚举也有 "Usb",就能自动匹配
|
|||
|
|
if (Enum.TryParse(brandApiString, true, out DeviceBrand result))
|
|||
|
|
{
|
|||
|
|
return (int)result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (int)DeviceBrand.Unknown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region ConvertBrandToEditString
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// [写] DTO 枚举值 (int) -> API 字符串
|
|||
|
|
/// 保存时,将 int 转换为 WebAPI 需要的字符串标识
|
|||
|
|
/// </summary>
|
|||
|
|
private string ConvertBrandToEditString(int brandValue)
|
|||
|
|
{
|
|||
|
|
// 将 int 强转为枚举,方便 switch
|
|||
|
|
var brand = (DeviceBrand)brandValue;
|
|||
|
|
|
|||
|
|
switch (brand)
|
|||
|
|
{
|
|||
|
|
case DeviceBrand.HikVision:
|
|||
|
|
return "HikVision"; // API 期望的字符串
|
|||
|
|
|
|||
|
|
case DeviceBrand.Dahua:
|
|||
|
|
return "Dahua";
|
|||
|
|
|
|||
|
|
case DeviceBrand.RtspGeneral:
|
|||
|
|
return "RTSP"; // 假设 API 期望全大写
|
|||
|
|
|
|||
|
|
case DeviceBrand.Usb:
|
|||
|
|
return "Usb";
|
|||
|
|
|
|||
|
|
case DeviceBrand.WebSocketShine:
|
|||
|
|
return "WebSocket"; // 需确认 API 期望什么
|
|||
|
|
|
|||
|
|
case DeviceBrand.File:
|
|||
|
|
return "File";
|
|||
|
|
|
|||
|
|
case DeviceBrand.OnvifGeneral:
|
|||
|
|
return "Onvif";
|
|||
|
|
|
|||
|
|
case DeviceBrand.Unknown:
|
|||
|
|
default:
|
|||
|
|
return "Unknown";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region MapDtoToEditJson
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 辅助方法:将 DTO 转换为 API 需要的 JSON 字符串
|
|||
|
|
/// </summary>
|
|||
|
|
private string MapDtoToEditJson(CameraEditInfo dto)
|
|||
|
|
{
|
|||
|
|
// 根据后端约定的最新格式构建匿名对象
|
|||
|
|
// 特点:
|
|||
|
|
// 1. brand 直接传 int
|
|||
|
|
// 2. 扩展参数(useGrayscale等) 直接放在根节点,不需要 vendorArguments 包裹
|
|||
|
|
var apiModel = new
|
|||
|
|
{
|
|||
|
|
id = dto.Id,
|
|||
|
|
name = dto.Name ?? "",
|
|||
|
|
|
|||
|
|
// ★ 修正1:直接使用 int 值,不再转换成 "HikVision" 字符串
|
|||
|
|
brand = dto.Brand,
|
|||
|
|
|
|||
|
|
location = dto.Location ?? "",
|
|||
|
|
ipAddress = dto.IpAddress,
|
|||
|
|
port = dto.Port,
|
|||
|
|
username = dto.Username ?? "",
|
|||
|
|
password = dto.Password ?? "",
|
|||
|
|
|
|||
|
|
// ★ 修正2:根据样例,renderHandle 传 0 (通常这是运行时句柄,保存时无意义)
|
|||
|
|
renderHandle = 0,
|
|||
|
|
|
|||
|
|
channelIndex = dto.ChannelIndex,
|
|||
|
|
rtspPath = dto.RtspPath ?? "",
|
|||
|
|
mainboardIp = dto.MainboardIp ?? "",
|
|||
|
|
mainboardPort = dto.MainboardPort,
|
|||
|
|
streamType = dto.StreamType,
|
|||
|
|
|
|||
|
|
// ★ 修正3:扁平化处理,直接放在根层级
|
|||
|
|
useGrayscale = dto.UseGrayscale,
|
|||
|
|
enhanceImage = dto.EnhanceImage,
|
|||
|
|
allowCompress = dto.AllowCompress,
|
|||
|
|
allowExpand = dto.AllowExpand,
|
|||
|
|
targetResolution = dto.TargetResolution ?? "",
|
|||
|
|
|
|||
|
|
allowShrink = dto.AllowShrink,
|
|||
|
|
allowEnlarge = dto.AllowEnlarge,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return JsonHelper.Serialize(apiModel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
}
|