具备界面基础功能

This commit is contained in:
2026-01-01 22:40:32 +08:00
parent 0c86b4dad3
commit d039559402
81 changed files with 8333 additions and 1905 deletions

View File

@@ -0,0 +1,105 @@
namespace SHH.CameraDashboard;
public partial class CameraRepository
{
#region GetListByAddressAsync
/// <summary>
/// [新增] 动态检测方法。
/// 专门用于向导界面,根据临时输入的 IP 地址和端口号来获取摄像头列表。
/// </summary>
/// <param name="ipAddress">目标服务器的 IP 地址。</param>
/// <param name="port">目标服务器的端口号。</param>
/// <param name="pageName">调用此方法的页面或步骤名称,用于日志记录。</param>
/// <returns>
/// 一个异步任务,其结果包含一个 <see cref="WebApiCameraModel"/> 对象的列表。
/// - 如果成功获取到列表,返回该列表(可能为空)。
/// - 如果获取过程中发生任何异常(如网络错误、服务器无响应等),返回 <c>null</c>。
/// </returns>
public async Task<List<WebApiCameraModel>?> GetListByAddressAsync(string ipAddress, string port, string pageName)
{
// 1. 统一管理并拼接请求 URL
// 在这里将基础地址与 API 路径组合ViewModel 无需关心具体的路由细节。
// WebApiRoutes.Cameras.Root 是一个假设的常量,例如 "/api/Cameras"。
string requestUrl = $"http://{ipAddress}:{port}{WebApiRoutes.Cameras.Root}";
try
{
// 2. 调用底层的 WebApiService 发送 GET 请求
// 传入 "Wizard" 相关的模块名,便于在日志中区分请求来源。
string jsonResponse = await WebApiService.Instance.GetAsync(requestUrl, moduleName: $"摄像头列表-{pageName}", default, true);
// 3. 使用 JsonHelper 进行反序列化
// 利用 JsonHelper 的防御性实现,即使 JSON 格式错误或内容为 "null",也不会抛出异常。
var cameraList = JsonHelper.Deserialize<List<WebApiCameraModel>>(jsonResponse);
// 4. 确保返回的列表不为 null
// 如果反序列化结果为 null例如API 返回 "null"),则返回一个空列表,
// 这样调用方可以安全地遍历,而无需进行 null 检查。
return cameraList ?? new List<WebApiCameraModel>();
}
catch (Exception)
{
// 5. 捕获所有异常
// 在动态检测阶段,网络不通、服务器宕机等都是预期内的情况。
// 返回 null 作为明确的失败信号ViewModel 可以据此向用户显示“连接失败”等提示。
return null;
}
}
#endregion
#region ControlPowerAsync
/// <summary>
/// 调用服务端的接口来控制指定摄像头的启停状态(开机/关机)。
/// </summary>
/// <param name="ipAddress">目标服务器的 IP 地址。</param>
/// <param name="port">目标服务器的端口号。</param>
/// <param name="cameraId">要控制的摄像头的唯一ID。</param>
/// <param name="enable">true 表示开机false 表示关机。</param>
/// <param name="pageName">调用此方法的页面或功能名称,用于日志记录。</param>
/// <returns>
/// 一个异步任务,其结果为一个布尔值:
/// - <c>true</c>表示操作成功HTTP 响应为 200 OK
/// - <c>false</c>:表示操作失败(如网络错误、服务器返回 4xx/5xx 错误等)。
/// </returns>
public async Task<bool> ControlPowerAsync(long cameraId, bool enable, string pageName)
{
var useServiceNode = AppGlobal.UseServiceNode;
if (useServiceNode == null)
return false;
var ipAddress = useServiceNode.ServiceNodeIp;
var port = useServiceNode.ServiceNodePort;
// 1. 拼接请求 URL
// 将控制参数cameraId 和 enable作为 URL 的一部分发送。
string requestUrl = $"http://{ipAddress}:{port}/api/Cameras/{cameraId}/power?enabled={enable}";
try
{
// 2. 调用底层服务发送 POST 请求
// 关键点:
// a. 即使服务端不关心请求体Body`PostAsync` 方法也要求一个非 null 的 JSON 字符串。
// 因此,我们传入一个空的 JSON 对象 "{}" 作为占位符。
// b. 我们不检查响应内容,因为如果请求失败(例如,服务器返回 404 Not Found 或 500 Internal Server Error
// `WebApiService` 会抛出异常。
await WebApiService.Instance.PostAsync(requestUrl, "{}", moduleName: $"设备启停-{pageName}");
// 3. 如果代码执行到这里,说明请求成功且没有抛出异常。
return true;
}
catch (Exception ex)
{
// 4. 捕获所有可能发生的异常
// 这包括网络连接问题、DNS解析失败、服务器无响应或返回错误状态码等。
// 记录异常信息到调试输出,以便于排查问题。
System.Diagnostics.Debug.WriteLine($"控制摄像头电源失败 [IP: {ipAddress}, CameraId: {cameraId}]: {ex.Message}");
// 返回 false 通知调用方操作已失败。
return false;
}
}
#endregion
}

View File

@@ -0,0 +1,396 @@
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
}
}

View File

@@ -0,0 +1,125 @@
using System.Text.Json;
namespace SHH.CameraDashboard
{
public partial class CameraRepository
{
public async Task<bool> UpdateImageProcessingAsync(CameraEditInfo dto)
{
if (dto == null) return false;
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return false;
// 1. 使用专用的 Processing 路由
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Processing(dto.Id.ToString())}";
// 2. 解析分辨率字符串 (例如 "1920x1080" -> 1920, 1080)
int width = 1280; // 默认值
int height = 720;
if (!string.IsNullOrWhiteSpace(dto.TargetResolution))
{
var parts = dto.TargetResolution.ToLower().Split('x');
if (parts.Length == 2)
{
int.TryParse(parts[0], out width);
int.TryParse(parts[1], out height);
}
}
try
{
// 2. 构建精简的专用 JSON Payload
// 只包含图像处理相关的字段,不发送其他干扰信息
var payload = new
{
// --- 分辨率 (拆分成 int) ---
targetWidth = width,
targetHeight = height,
// 确保这些字段名与后端 Processing 接口的定义一致
useGrayscale = dto.UseGrayscale,
enhanceImage = dto.EnhanceImage,
// 缩放控制
allowShrink = dto.AllowShrink,
allowEnlarge = dto.AllowEnlarge, // 这里使用你确认过的 AllowEnlarge
// 目标分辨率
targetResolution = dto.TargetResolution ?? ""
};
string jsonBody = JsonHelper.Serialize(payload);
// 3. 发送请求
// 通常专用功能接口使用 POST 或 PUT。根据你的路由命名(Processing 是名词/动词)
// 且是对资源的部分修改PUT 的可能性较大;但如果是“执行处理”,也可能是 POST。
// 建议先试 PUT (因为是 Update 操作),如果报 405 则改 POST。
await WebApiService.Instance.PostAsync(requestUrl, jsonBody, "图像处理配置");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新图像处理配置失败: {ex.Message}");
return false;
}
}
/// <summary>
/// [新增] 获取指定设备的图像处理配置
/// 对应后端: GET /api/cameras/{id}/processing
/// </summary>
public async Task<CameraEditInfo> GetImageProcessingAsync(long id)
{
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return null;
// 1. 拼接 URL: http://.../api/Cameras/1001/processing
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{id}/processing";
try
{
// 2. 发送 GET 请求
string jsonResponse = await WebApiService.Instance.GetAsync(requestUrl, "获取图像处理配置");
if (string.IsNullOrEmpty(jsonResponse)) return null;
// 3. 解析后端返回的 ProcessingOptions JSON
// 注意:这里定义一个临时类来匹配后端 JSON 结构,确保字段名一致
var options = JsonHelper.Deserialize<ProcessingOptionsDto>(jsonResponse);
if (options == null) return null;
// 4. 映射回 CameraEditInfo 给 ViewModel 使用
return new CameraEditInfo
{
Id = id,
// 将宽高拼回 "1920x1080" 格式
TargetResolution = $"{options.targetWidth}x{options.targetHeight}",
// 映射开关 (注意字段名对应关系)
AllowShrink = options.enableShrink,
AllowEnlarge = options.enableExpand, // 后端叫 EnableExpand
EnhanceImage = options.enableBrightness
};
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[Repository] 获取图像配置失败: {ex.Message}");
return null;
}
}
// [内部类] 用于接收后端 JSON 的 DTO
// 字段名需与后端 ProcessingOptions 序列化出来的 JSON 字段一致 (通常是 camelCase)
private class ProcessingOptionsDto
{
public int targetWidth { get; set; }
public int targetHeight { get; set; }
public bool enableShrink { get; set; }
public bool enableExpand { get; set; } // 对应后端的 EnableExpand
public bool enableBrightness { get; set; } // 对应后端的 EnableBrightness
public int brightness { get; set; }
}
}
}

View File

@@ -0,0 +1,232 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace SHH.CameraDashboard
{
public partial class CameraRepository
{
// 文件: Services\WebApis\CameraReps\CameraRepository.cs
public async Task<bool> UpdateSubscriptionAsync(long cameraId, SubscriptionDto dto)
{
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return false;
// URL: POST /api/Cameras/{id}/subscriptions
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{cameraId}/subscriptions";
try
{
string jsonBody = JsonHelper.Serialize(dto);
// 发送 POST 请求
await WebApiService.Instance.PostAsync(requestUrl, jsonBody, "更新订阅配置");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新订阅失败: {ex.Message}");
return false;
}
}
/// <summary>
/// [新增] 获取订阅列表
/// </summary>
public async Task<List<SubscriptionDto>> GetSubscriptionsAsync(long cameraId)
{
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return new List<SubscriptionDto>();
// URL: GET /api/Cameras/{id}/subscriptions
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{cameraId}/subscriptions";
try
{
string json = await WebApiService.Instance.GetAsync(requestUrl, "获取订阅列表");
// 如果返回空或null返回空列表
if (string.IsNullOrEmpty(json)) return new List<SubscriptionDto>();
var list = JsonHelper.Deserialize<List<SubscriptionDto>>(json);
return list ?? new List<SubscriptionDto>();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"获取订阅列表失败: {ex.Message}");
return new List<SubscriptionDto>();
}
}
/// <summary>
/// [修改] 删除/注销订阅
/// 改为标准的 DELETE 请求
/// </summary>
public async Task<bool> DeleteSubscriptionAsync(long cameraId, string appId)
{
if (string.IsNullOrWhiteSpace(appId)) return false;
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return false;
// 拼接 URL: DELETE /api/Cameras/1001/subscriptions/Client_01
// 注意AppId 如果包含特殊字符,建议 UrlEncode但一般 ID 都是字母数字
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Root}/{cameraId}/subscriptions/{appId}";
try
{
// 调用刚刚在 WebApiService 里加的 DeleteAsync
await WebApiService.Instance.DeleteAsync(requestUrl, "注销订阅");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"注销订阅失败: {ex.Message}");
return false;
}
}
}
/// <summary>
/// 视频流订阅配置请求对象
/// 用于定义第三方应用或内部模块对指定相机流的消费需求
/// </summary>
public class SubscriptionDto
{
/// <summary>
/// 进程唯一标识 (如 "AI_Process_01"、"Main_Display_02")
/// </summary>
public string AppId { get; set; } = string.Empty;
/// <summary>
/// 订阅业务类型。
/// 决定了后端流控引擎后续的资源分配(如是否开启录像机或渲染器)。
/// </summary>
public int Type { get; set; }
/// <summary>
/// 显示帧率需求 (单位: fps)
/// <para>不需要显示则设为 0控制器会自动注销该类型需求</para>
/// </summary>
public int DisplayFps
{
get => TargetFps;
set => TargetFps = value;
}
public int TargetFps { get; set; }
/// <summary>
/// [新增] 实际显示/分发帧率
/// JSON: "realFps"
/// </summary>
[JsonPropertyName("realFps")]
public double RealFps { get; set; }
/// <summary>
/// 备注信息。
/// 用于记录订阅的用途、申请人或关联业务系统。
/// </summary>
public string Memo { get; set; }
= string.Empty;
/// <summary>
/// 窗口句柄HWND
/// 仅在 Type 为 HandleDisplay 时必填。格式通常为十六进制或十进制字符串。
/// </summary>
public string Handle { get; set; }
= string.Empty;
/// <summary>
/// 录像持续时长(分钟,范围 1-60
/// 仅在 Type 为 LocalRecord 时有效。
/// </summary>
public int RecordDuration { get; set; }
/// <summary>
/// 录像文件存放绝对路径。
/// 仅在 Type 为 LocalRecord 时有效例如C:\Recordings\Room01。
/// </summary>
public string SavePath { get; set; }
= string.Empty;
/// <summary>
/// 通讯方式协议。
/// 仅在 Type 为 NetworkTrans 或 WebPush 时有效,默认为 Network。
/// </summary>
public int Protocol { get; set; }
/// <summary>
/// 目标接收端 IP 地址。
/// 仅在 Type 为 NetworkTrans 或 WebPush 且 Protocol 为 Network 时必填。
/// </summary>
public string TargetIp { get; set; }
= string.Empty;
/// <summary>
/// 目标接收端端口号。
/// 仅在 Type 为 NetworkTrans 或 WebPush 时必填。
/// </summary>
public int TargetPort { get; set; }
}
/// <summary>
/// 订阅业务类型枚举
/// 描述视频流的最终去向和业务用途,用于帧分发策略的路由决策
/// </summary>
public enum SubscriptionType
{
/// <summary>
/// 本地窗口渲染
/// <para>直接在服务器端显示器绘制(如 OpenCV Window、WinForm 控件)</para>
/// </summary>
[Description("本地窗口显示")]
LocalWindow = 0,
/// <summary>
/// 本地录像存储
/// <para>写入磁盘文件(如 MP4/AVI 格式,支持定时切割、循环覆盖)</para>
/// </summary>
[Description("本地录像存储")]
LocalRecord = 1,
/// <summary>
/// 句柄绑定显示
/// <para>渲染到指定 HWND 窗口句柄(如 SDK 硬件解码渲染到客户端控件)</para>
/// </summary>
[Description("句柄绑定显示")]
HandleDisplay = 2,
/// <summary>
/// 自定义网络传输
/// <para>通过私有协议转发给第三方系统(如工控机、告警服务器)</para>
/// </summary>
[Description("自定义网络传输")]
NetworkTrans = 3,
/// <summary>
/// 网页端推流
/// <para>转码为 Web 标准协议(如 WebRTC、HLS、RTMP供浏览器播放</para>
/// </summary>
[Description("网页端推流")]
WebPush = 4
}
/// <summary>
/// 网络传输协议类型
/// </summary>
public enum TransportProtocol
{
/// <summary> 可靠传输 (默认) </summary>
Tcp = 0,
/// <summary> 快速传输 (可能丢包/花屏) </summary>
Udp = 1,
/// <summary> 组播 (节省带宽) </summary>
Multicast = 2,
/// <summary> 内存交互 </summary>
Memory = 99,
}
}

View File

@@ -0,0 +1,90 @@
namespace SHH.CameraDashboard;
public partial class CameraRepository
{
#region PtzControlAsync
/// <summary>
/// 发送 PTZ 控制指令
/// </summary>
public async Task<bool> PtzControlAsync(long cameraId, PtzControlDto payload)
{
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return false;
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Ptz(cameraId.ToString())}";
try
{
string json = JsonHelper.Serialize(payload);
await WebApiService.Instance.PostAsync(requestUrl, json, "PTZ控制");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"PTZ控制失败: {ex.Message}");
return false;
}
}
#endregion
#region (Maintenance)
/// <summary>
/// [修正] 发送校时指令
/// 修正点:后端 [FromBody] DateTime 需要接收带双引号的标准 ISO 时间字符串
/// </summary>
public async Task<bool> SyncTimeAsync(long cameraId)
{
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return false;
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Time(cameraId.ToString())}";
try
{
// 1. 获取当前时间,格式化为标准 ISO 8601 (yyyy-MM-ddTHH:mm:ss)
string timeStr = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
// 2. [关键] 手动构造 JSON Body必须包含双引号
// 例如发送的内容应该是: "2025-01-01T12:00:00"
string jsonBody = $"\"{timeStr}\"";
// 3. 发送请求
// 确保你的 WebApiService.PostAsync 会设置 Content-Type: application/json
await WebApiService.Instance.PostAsync(requestUrl, jsonBody, "设备校时");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"校时失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 发送远程重启指令
/// 接口定义: [HttpPost("{id}/reboot")] public async Task<IActionResult> RebootDevice(long id)
/// 该接口没有 [FromBody] 参数Body 可以为空或空 JSON
/// </summary>
public async Task<bool> RebootCameraAsync(long cameraId)
{
var serviceNode = AppGlobal.UseServiceNode;
if (serviceNode == null) return false;
string requestUrl = $"http://{serviceNode.ServiceNodeIp}:{serviceNode.ServiceNodePort}{WebApiRoutes.Cameras.Reboot(cameraId.ToString())}";
try
{
await WebApiService.Instance.PostAsync(requestUrl, "{}", "远程重启");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"重启失败: {ex.Message}");
return false;
}
}
#endregion
}