574 lines
20 KiB
C#
574 lines
20 KiB
C#
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; }
|
||
}
|