574 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AmtScanner.Api.Data;
using AmtScanner.Api.Models;
using AmtScanner.Api.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Concurrent;
namespace AmtScanner.Api.Controllers;
[ApiController]
[Route("api/os-devices")]
public class OsDevicesController : ControllerBase
{
private readonly AppDbContext _context;
private readonly IWindowsScannerService _scannerService;
private readonly ILogger<OsDevicesController> _logger;
private static readonly ConcurrentDictionary<string, OsScanProgress> _scanProgress = new();
public OsDevicesController(
AppDbContext context,
IWindowsScannerService scannerService,
ILogger<OsDevicesController> logger)
{
_context = context;
_scannerService = scannerService;
_logger = logger;
}
/// <summary>
/// 获取所有操作系统设备
/// </summary>
[HttpGet]
public async Task<ActionResult<ApiResponse<List<OsDeviceDto>>>> GetAll()
{
var devices = await _context.OsDevices
.Include(o => o.AmtDevice)
.OrderByDescending(o => o.LastUpdatedAt)
.Select(o => new OsDeviceDto
{
Id = o.Id,
IpAddress = o.IpAddress,
SystemUuid = o.SystemUuid,
Hostname = o.Hostname,
OsType = o.OsType.ToString(),
OsVersion = o.OsVersion,
Architecture = o.Architecture,
LoggedInUser = o.LoggedInUser,
LastBootTime = o.LastBootTime,
MacAddress = o.MacAddress,
IsOnline = o.IsOnline,
LastOnlineAt = o.LastOnlineAt,
DiscoveredAt = o.DiscoveredAt,
LastUpdatedAt = o.LastUpdatedAt,
Description = o.Description,
WindowsUsername = o.WindowsUsername,
WindowsPassword = o.WindowsPassword,
AmtDeviceId = o.AmtDeviceId,
AmtDeviceIp = o.AmtDevice != null ? o.AmtDevice.IpAddress : null
})
.ToListAsync();
return Ok(ApiResponse<List<OsDeviceDto>>.Success(devices));
}
/// <summary>
/// 获取单个操作系统设备
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<OsDeviceDto>>> GetById(long id)
{
var device = await _context.OsDevices
.Include(o => o.AmtDevice)
.FirstOrDefaultAsync(o => o.Id == id);
if (device == null)
return Ok(ApiResponse<OsDeviceDto>.Fail(404, "设备不存在"));
return Ok(ApiResponse<OsDeviceDto>.Success(new OsDeviceDto
{
Id = device.Id,
IpAddress = device.IpAddress,
SystemUuid = device.SystemUuid,
Hostname = device.Hostname,
OsType = device.OsType.ToString(),
OsVersion = device.OsVersion,
Architecture = device.Architecture,
LoggedInUser = device.LoggedInUser,
LastBootTime = device.LastBootTime,
MacAddress = device.MacAddress,
IsOnline = device.IsOnline,
LastOnlineAt = device.LastOnlineAt,
DiscoveredAt = device.DiscoveredAt,
LastUpdatedAt = device.LastUpdatedAt,
Description = device.Description,
AmtDeviceId = device.AmtDeviceId,
AmtDeviceIp = device.AmtDevice?.IpAddress
}));
}
/// <summary>
/// 启动操作系统扫描
/// </summary>
[HttpPost("scan/start")]
public async Task<ActionResult<ApiResponse<OsScanStartResponse>>> StartScan(
[FromBody] OsScanRequest request)
{
var taskId = Guid.NewGuid().ToString("N");
// 初始化进度为 0%
_scanProgress[taskId] = new OsScanProgress
{
TaskId = taskId,
ScannedCount = 0,
TotalCount = 1, // 避免除以0
FoundDevices = 0,
ProgressPercentage = 0,
CurrentIp = "初始化中..."
};
// 使用 Action 回调直接更新进度
Action<OsScanProgress> progressCallback = p =>
{
_scanProgress[taskId] = p;
};
_ = Task.Run(async () =>
{
try
{
await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progressCallback);
}
catch (Exception ex)
{
_logger.LogError(ex, "OS scan failed for task {TaskId}", taskId);
}
});
return Ok(ApiResponse<OsScanStartResponse>.Success(new OsScanStartResponse
{
TaskId = taskId,
Message = "操作系统扫描已启动"
}));
}
/// <summary>
/// 获取扫描进度
/// </summary>
[HttpGet("scan/status/{taskId}")]
public ActionResult<ApiResponse<OsScanProgress>> GetScanStatus(string taskId)
{
if (_scanProgress.TryGetValue(taskId, out var progress))
{
return Ok(ApiResponse<OsScanProgress>.Success(progress));
}
// 任务不存在时返回 -1 表示任务不存在,前端应该停止轮询
return Ok(ApiResponse<OsScanProgress>.Success(new OsScanProgress
{
TaskId = taskId,
ScannedCount = 0,
TotalCount = 0,
FoundDevices = 0,
ProgressPercentage = -1, // -1 表示任务不存在
CurrentIp = null
}));
}
/// <summary>
/// 取消扫描
/// </summary>
[HttpPost("scan/cancel/{taskId}")]
public ActionResult<ApiResponse<object>> CancelScan(string taskId)
{
_scannerService.CancelScan(taskId);
return Ok(ApiResponse<object>.Success(null, "扫描已取消"));
}
/// <summary>
/// 获取扫描发现的设备列表(未保存到数据库)
/// </summary>
[HttpGet("scan/results/{taskId}")]
public ActionResult<ApiResponse<List<ScanResultDto>>> GetScanResults(string taskId)
{
_logger.LogInformation("Getting scan results for task: {TaskId}", taskId);
var devices = _scannerService.GetScanResults(taskId);
_logger.LogInformation("Found {Count} devices in scan results for task: {TaskId}", devices.Count, taskId);
var results = devices.Select(d => new ScanResultDto
{
IpAddress = d.IpAddress,
OsType = d.OsType.ToString(),
Hostname = d.Hostname,
IsOnline = d.IsOnline,
DiscoveredAt = d.DiscoveredAt
}).ToList();
return Ok(ApiResponse<List<ScanResultDto>>.Success(results));
}
/// <summary>
/// 保存选中的设备到数据库
/// </summary>
[HttpPost("scan/save")]
public async Task<ActionResult<ApiResponse<SaveDevicesResponse>>> SaveSelectedDevices(
[FromBody] SaveDevicesRequest request)
{
if (string.IsNullOrEmpty(request.TaskId) || request.SelectedIps == null || request.SelectedIps.Count == 0)
{
return Ok(ApiResponse<SaveDevicesResponse>.Fail(400, "请选择要添加的设备"));
}
var savedCount = await _scannerService.SaveSelectedDevicesAsync(request.TaskId, request.SelectedIps);
// 清除扫描结果
_scannerService.ClearScanResults(request.TaskId);
return Ok(ApiResponse<SaveDevicesResponse>.Success(new SaveDevicesResponse
{
SavedCount = savedCount,
Message = $"成功添加 {savedCount} 台设备"
}));
}
/// <summary>
/// 获取设备详细信息(通过 WMI
/// </summary>
[HttpPost("{id}/fetch-info")]
public async Task<ActionResult<ApiResponse<OsDeviceDto>>> FetchDeviceInfo(
long id,
[FromBody] WmiCredentials credentials)
{
var device = await _context.OsDevices.FindAsync(id);
if (device == null)
return Ok(ApiResponse<OsDeviceDto>.Fail(404, "设备不存在"));
try
{
var osInfo = await _scannerService.GetOsInfoAsync(
device.IpAddress,
credentials.Username,
credentials.Password);
if (osInfo == null)
return Ok(ApiResponse<OsDeviceDto>.Fail(500, "无法获取系统信息。可能原因1) 目标机器WMI服务未启动 2) 防火墙阻止连接 3) 凭据不正确 4) 目标机器不允许远程WMI连接"));
// 更新设备信息
device.SystemUuid = osInfo.SystemUuid;
device.Hostname = osInfo.Hostname;
device.OsVersion = osInfo.OsVersion;
device.Architecture = osInfo.Architecture;
device.LoggedInUser = osInfo.LoggedInUser;
device.LastBootTime = osInfo.LastBootTime;
device.MacAddress = osInfo.MacAddress;
device.LastUpdatedAt = DateTime.UtcNow;
device.Description = "通过 WMI 获取详细信息";
await _context.SaveChangesAsync();
// 尝试绑定 AMT 设备
await _scannerService.BindAmtDevicesAsync();
// 重新加载以获取关联的 AMT 设备
await _context.Entry(device).Reference(d => d.AmtDevice).LoadAsync();
return Ok(ApiResponse<OsDeviceDto>.Success(new OsDeviceDto
{
Id = device.Id,
IpAddress = device.IpAddress,
SystemUuid = device.SystemUuid,
Hostname = device.Hostname,
OsType = device.OsType.ToString(),
OsVersion = device.OsVersion,
Architecture = device.Architecture,
LoggedInUser = device.LoggedInUser,
LastBootTime = device.LastBootTime,
MacAddress = device.MacAddress,
IsOnline = device.IsOnline,
LastOnlineAt = device.LastOnlineAt,
DiscoveredAt = device.DiscoveredAt,
LastUpdatedAt = device.LastUpdatedAt,
Description = device.Description,
AmtDeviceId = device.AmtDeviceId,
AmtDeviceIp = device.AmtDevice?.IpAddress
}, "系统信息已更新"));
}
catch (Exception ex)
{
_logger.LogError(ex, "获取设备 {Id} 系统信息失败", id);
return Ok(ApiResponse<OsDeviceDto>.Fail(500, $"获取系统信息失败: {ex.Message}"));
}
}
/// <summary>
/// 手动绑定 AMT 设备
/// </summary>
[HttpPost("{id}/bind-amt/{amtDeviceId}")]
public async Task<ActionResult<ApiResponse<object>>> BindAmtDevice(long id, long amtDeviceId)
{
var osDevice = await _context.OsDevices.FindAsync(id);
if (osDevice == null)
return Ok(ApiResponse<object>.Fail(404, "操作系统设备不存在"));
var amtDevice = await _context.AmtDevices.FindAsync(amtDeviceId);
if (amtDevice == null)
return Ok(ApiResponse<object>.Fail(404, "AMT 设备不存在"));
osDevice.AmtDeviceId = amtDeviceId;
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "绑定成功"));
}
/// <summary>
/// 解除 AMT 绑定
/// </summary>
[HttpPost("{id}/unbind-amt")]
public async Task<ActionResult<ApiResponse<object>>> UnbindAmtDevice(long id)
{
var osDevice = await _context.OsDevices.FindAsync(id);
if (osDevice == null)
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
osDevice.AmtDeviceId = null;
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "已解除绑定"));
}
/// <summary>
/// 自动绑定所有设备
/// </summary>
[HttpPost("auto-bind")]
public async Task<ActionResult<ApiResponse<object>>> AutoBindAll()
{
await _scannerService.BindAmtDevicesAsync();
return Ok(ApiResponse<object>.Success(null, "自动绑定完成"));
}
/// <summary>
/// 批量 AMT 绑定(通过 UUID 匹配)
/// </summary>
[HttpPost("batch-bind-amt")]
public async Task<ActionResult<ApiResponse<BatchBindAmtResponse>>> BatchBindAmt([FromBody] BatchBindAmtRequest request)
{
if (request.DeviceIds == null || request.DeviceIds.Count == 0)
{
return Ok(ApiResponse<BatchBindAmtResponse>.Fail(400, "请选择要绑定的设备"));
}
var results = new List<BindAmtResult>();
// 获取所有 AMT 设备,用于 UUID 匹配
var amtDevices = await _context.AmtDevices.ToListAsync();
var amtDevicesByUuid = amtDevices
.Where(a => !string.IsNullOrEmpty(a.SystemUuid))
.ToDictionary(a => a.SystemUuid!.ToUpperInvariant(), a => a);
var amtDevicesByIp = amtDevices.ToDictionary(a => a.IpAddress, a => a);
foreach (var deviceId in request.DeviceIds)
{
var result = new BindAmtResult { DeviceId = deviceId };
try
{
var osDevice = await _context.OsDevices.FindAsync(deviceId);
if (osDevice == null)
{
result.Success = false;
result.Error = "设备不存在";
results.Add(result);
continue;
}
result.IpAddress = osDevice.IpAddress;
result.PreviousAmtDeviceId = osDevice.AmtDeviceId;
AmtDevice? matchedAmtDevice = null;
string matchMethod = "";
// 优先通过 UUID 匹配
if (!string.IsNullOrEmpty(osDevice.SystemUuid))
{
var uuidKey = osDevice.SystemUuid.ToUpperInvariant();
if (amtDevicesByUuid.TryGetValue(uuidKey, out var amtDevice))
{
matchedAmtDevice = amtDevice;
matchMethod = "UUID";
}
}
// 如果 UUID 匹配失败,尝试通过 IP 匹配
if (matchedAmtDevice == null)
{
if (amtDevicesByIp.TryGetValue(osDevice.IpAddress, out var amtDevice))
{
matchedAmtDevice = amtDevice;
matchMethod = "IP";
}
}
if (matchedAmtDevice != null)
{
osDevice.AmtDeviceId = matchedAmtDevice.Id;
await _context.SaveChangesAsync();
result.Success = true;
result.AmtDeviceId = matchedAmtDevice.Id;
result.AmtDeviceIp = matchedAmtDevice.IpAddress;
result.MatchMethod = matchMethod;
_logger.LogInformation("Bound OS device {OsIp} to AMT device {AmtIp} via {Method}",
osDevice.IpAddress, matchedAmtDevice.IpAddress, matchMethod);
}
else
{
result.Success = false;
result.Error = "未找到匹配的 AMT 设备(需要相同的 UUID 或 IP";
}
}
catch (Exception ex)
{
result.Success = false;
result.Error = ex.Message;
_logger.LogWarning(ex, "Failed to bind AMT for device {DeviceId}", deviceId);
}
results.Add(result);
}
var successCount = results.Count(r => r.Success);
var failCount = results.Count(r => !r.Success);
return Ok(ApiResponse<BatchBindAmtResponse>.Success(new BatchBindAmtResponse
{
Results = results,
SuccessCount = successCount,
FailCount = failCount
}, $"成功绑定 {successCount} 台设备,失败 {failCount} 台"));
}
/// <summary>
/// 设置 Windows 登录凭据
/// </summary>
[HttpPut("{id}/credentials")]
public async Task<ActionResult<ApiResponse<object>>> SetCredentials(long id, [FromBody] WindowsCredentialsRequest request)
{
var device = await _context.OsDevices.FindAsync(id);
if (device == null)
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
device.WindowsUsername = request.Username;
device.WindowsPassword = request.Password; // TODO: 加密存储
device.LastUpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "凭据已保存"));
}
/// <summary>
/// 删除设备
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult<ApiResponse<object>>> Delete(long id)
{
var device = await _context.OsDevices.FindAsync(id);
if (device == null)
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
_context.OsDevices.Remove(device);
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "删除成功"));
}
}
public class OsDeviceDto
{
public long Id { get; set; }
public string IpAddress { get; set; } = string.Empty;
public string? SystemUuid { get; set; }
public string? Hostname { get; set; }
public string? OsType { get; set; }
public string? OsVersion { get; set; }
public string? Architecture { get; set; }
public string? LoggedInUser { get; set; }
public DateTime? LastBootTime { get; set; }
public string? MacAddress { get; set; }
public bool IsOnline { get; set; }
public DateTime? LastOnlineAt { get; set; }
public DateTime DiscoveredAt { get; set; }
public DateTime LastUpdatedAt { get; set; }
public string? Description { get; set; }
public string? WindowsUsername { get; set; }
public string? WindowsPassword { get; set; }
public long? AmtDeviceId { get; set; }
public string? AmtDeviceIp { get; set; }
}
public class WmiCredentials
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class OsScanStartResponse
{
public string TaskId { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public class OsScanRequest
{
public string NetworkSegment { get; set; } = string.Empty;
public string SubnetMask { get; set; } = string.Empty;
}
public class ScanResultDto
{
public string IpAddress { get; set; } = string.Empty;
public string OsType { get; set; } = string.Empty;
public string? Hostname { get; set; }
public bool IsOnline { get; set; }
public DateTime DiscoveredAt { get; set; }
}
public class SaveDevicesRequest
{
public string TaskId { get; set; } = string.Empty;
public List<string> SelectedIps { get; set; } = new();
}
public class SaveDevicesResponse
{
public int SavedCount { get; set; }
public string Message { get; set; } = string.Empty;
}
public class WindowsCredentialsRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
/// <summary>
/// 批量 AMT 绑定请求
/// </summary>
public class BatchBindAmtRequest
{
public List<long> DeviceIds { get; set; } = new();
}
/// <summary>
/// 批量 AMT 绑定响应
/// </summary>
public class BatchBindAmtResponse
{
public List<BindAmtResult> Results { get; set; } = new();
public int SuccessCount { get; set; }
public int FailCount { get; set; }
}
/// <summary>
/// 单个设备 AMT 绑定结果
/// </summary>
public class BindAmtResult
{
public long DeviceId { get; set; }
public string? IpAddress { get; set; }
public bool Success { get; set; }
public long? PreviousAmtDeviceId { get; set; }
public long? AmtDeviceId { get; set; }
public string? AmtDeviceIp { get; set; }
public string? MatchMethod { get; set; }
public string? Error { get; set; }
}