From 8acd7b0ab6d472e1a45eeac4ab4b5db62b8fbff4 Mon Sep 17 00:00:00 2001 From: lvfengfree Date: Wed, 21 Jan 2026 21:24:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E6=A1=8C=E9=9D=A2?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=20-=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=B7=BB=E5=8A=A0=E5=92=8C=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E8=8F=9C=E5=8D=95=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E9=85=8D=E7=BD=AEWindows=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=92=8C=E5=88=A0=E9=99=A4=E8=AE=BE=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSystem/src/api/amt.ts | 25 +- .../src/views/desktop-manage/devices.vue | 319 ++++++++++---- .../src/views/desktop-manage/os-devices.vue | 396 +++++++----------- .../Controllers/OsDevicesController.cs | 124 +++++- .../AmtScanner.Api/Data/DbSeeder.cs | 3 +- .../AmtScanner.Api/Models/OsDevice.cs | 10 + .../Services/AmtPowerService.cs | 7 +- .../Services/WindowsScannerService.cs | 84 +++- .../add_os_device_credentials.sql | 4 + .../AmtScanner.Api/update_desktop_menu.sql | 25 ++ 10 files changed, 667 insertions(+), 330 deletions(-) create mode 100644 backend-csharp/AmtScanner.Api/add_os_device_credentials.sql create mode 100644 backend-csharp/AmtScanner.Api/update_desktop_menu.sql diff --git a/adminSystem/src/api/amt.ts b/adminSystem/src/api/amt.ts index f64f57d..215097b 100644 --- a/adminSystem/src/api/amt.ts +++ b/adminSystem/src/api/amt.ts @@ -341,7 +341,7 @@ export const osDeviceApi = { startScan(networkSegment: string, subnetMask: string) { return request.post({ url: '/api/os-devices/scan/start', - params: { networkSegment, subnetMask } + data: { networkSegment, subnetMask } }) }, @@ -352,6 +352,21 @@ export const osDeviceApi = { }) }, + // 获取扫描结果(未保存的设备列表) + getScanResults(taskId: string) { + return request.get({ + url: `/api/os-devices/scan/results/${taskId}` + }) + }, + + // 保存选中的设备 + saveSelectedDevices(taskId: string, selectedIps: string[]) { + return request.post({ + url: '/api/os-devices/scan/save', + data: { taskId, selectedIps } + }) + }, + // 取消扫描 cancelScan(taskId: string) { return request.post({ @@ -392,6 +407,14 @@ export const osDeviceApi = { }) }, + // 设置 Windows 登录凭据 + setCredentials(id: number, credentials: { username: string; password: string }) { + return request.put({ + url: `/api/os-devices/${id}/credentials`, + data: credentials + }) + }, + // 删除设备 delete(id: number) { return request.del({ diff --git a/adminSystem/src/views/desktop-manage/devices.vue b/adminSystem/src/views/desktop-manage/devices.vue index 7c411f6..48f1740 100644 --- a/adminSystem/src/views/desktop-manage/devices.vue +++ b/adminSystem/src/views/desktop-manage/devices.vue @@ -3,7 +3,7 @@ + + +
+
+ + 已选择 {{ selectedDevices.length }} 台设备 + + 请勾选设备进行操作 +
+
+ + + 配置Windows账号 + + + + 删除 + +
+
- + + - - + + + + + + + - + - + - - + + + + + +
+ + + + 需要提供 Windows 管理员凭据通过 WMI 获取系统信息 + + + + + + + + + + + + - - - - + +
+ + 将为以下 {{ credentialsTargetDevices.length }} 台设备配置相同的 Windows 账号: +
{{ credentialsTargetDevices.map(d => d.ipAddress).join(', ') }}
+
+
+ + + @@ -102,26 +169,53 @@ diff --git a/backend-csharp/AmtScanner.Api/Controllers/OsDevicesController.cs b/backend-csharp/AmtScanner.Api/Controllers/OsDevicesController.cs index 0a86dd6..ce36450 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/OsDevicesController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/OsDevicesController.cs @@ -52,6 +52,8 @@ public class OsDevicesController : ControllerBase 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 }) @@ -104,16 +106,28 @@ public class OsDevicesController : ControllerBase { var taskId = Guid.NewGuid().ToString("N"); - var progress = new Progress(p => + // 初始化进度为 0% + _scanProgress[taskId] = new OsScanProgress + { + TaskId = taskId, + ScannedCount = 0, + TotalCount = 1, // 避免除以0 + FoundDevices = 0, + ProgressPercentage = 0, + CurrentIp = "初始化中..." + }; + + // 使用 Action 回调直接更新进度 + Action progressCallback = p => { _scanProgress[taskId] = p; - }); + }; _ = Task.Run(async () => { try { - await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progress); + await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progressCallback); } catch (Exception ex) { @@ -138,7 +152,16 @@ public class OsDevicesController : ControllerBase { return Ok(ApiResponse.Success(progress)); } - return Ok(ApiResponse.Fail(404, "扫描任务不存在")); + // 任务不存在时返回 -1 表示任务不存在,前端应该停止轮询 + return Ok(ApiResponse.Success(new OsScanProgress + { + TaskId = taskId, + ScannedCount = 0, + TotalCount = 0, + FoundDevices = 0, + ProgressPercentage = -1, // -1 表示任务不存在 + CurrentIp = null + })); } /// @@ -151,6 +174,52 @@ public class OsDevicesController : ControllerBase return Ok(ApiResponse.Success(null, "扫描已取消")); } + /// + /// 获取扫描发现的设备列表(未保存到数据库) + /// + [HttpGet("scan/results/{taskId}")] + public ActionResult>> 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>.Success(results)); + } + + /// + /// 保存选中的设备到数据库 + /// + [HttpPost("scan/save")] + public async Task>> SaveSelectedDevices( + [FromBody] SaveDevicesRequest request) + { + if (string.IsNullOrEmpty(request.TaskId) || request.SelectedIps == null || request.SelectedIps.Count == 0) + { + return Ok(ApiResponse.Fail(400, "请选择要添加的设备")); + } + + var savedCount = await _scannerService.SaveSelectedDevicesAsync(request.TaskId, request.SelectedIps); + + // 清除扫描结果 + _scannerService.ClearScanResults(request.TaskId); + + return Ok(ApiResponse.Success(new SaveDevicesResponse + { + SavedCount = savedCount, + Message = $"成功添加 {savedCount} 台设备" + })); + } + /// /// 获取设备详细信息(通过 WMI) /// @@ -266,6 +335,24 @@ public class OsDevicesController : ControllerBase return Ok(ApiResponse.Success(null, "自动绑定完成")); } + /// + /// 设置 Windows 登录凭据 + /// + [HttpPut("{id}/credentials")] + public async Task>> SetCredentials(long id, [FromBody] WindowsCredentialsRequest request) + { + var device = await _context.OsDevices.FindAsync(id); + if (device == null) + return Ok(ApiResponse.Fail(404, "设备不存在")); + + device.WindowsUsername = request.Username; + device.WindowsPassword = request.Password; // TODO: 加密存储 + device.LastUpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(ApiResponse.Success(null, "凭据已保存")); + } + /// /// 删除设备 /// @@ -300,6 +387,8 @@ public class OsDeviceDto 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; } } @@ -321,3 +410,30 @@ 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 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; +} diff --git a/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs b/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs index a9c82ba..0e1bb22 100644 --- a/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs +++ b/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs @@ -115,7 +115,8 @@ public static class DbSeeder // 桌面管理菜单(系统内置) new() { Id = 20, Name = "DesktopManage", Path = "/desktop-manage", Component = "/index/index", Title = "桌面管理", Icon = "ri:remote-control-line", Sort = 4, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, - new() { Id = 21, ParentId = 20, Name = "DesktopDevices", Path = "devices", Component = "/desktop-manage/devices", Title = "远程桌面", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 21, ParentId = 20, Name = "OsDevices", Path = "os-devices", Component = "/desktop-manage/os-devices", Title = "系统添加", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 22, ParentId = 20, Name = "DesktopDevices", Path = "devices", Component = "/desktop-manage/devices", Title = "系统管理", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, // 系统管理菜单(系统内置) new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, diff --git a/backend-csharp/AmtScanner.Api/Models/OsDevice.cs b/backend-csharp/AmtScanner.Api/Models/OsDevice.cs index 59e7579..34c1351 100644 --- a/backend-csharp/AmtScanner.Api/Models/OsDevice.cs +++ b/backend-csharp/AmtScanner.Api/Models/OsDevice.cs @@ -82,6 +82,16 @@ public class OsDevice /// public string? Description { get; set; } + /// + /// Windows 登录用户名 + /// + public string? WindowsUsername { get; set; } + + /// + /// Windows 登录密码(加密存储) + /// + public string? WindowsPassword { get; set; } + /// /// 关联的 AMT 设备 ID /// diff --git a/backend-csharp/AmtScanner.Api/Services/AmtPowerService.cs b/backend-csharp/AmtScanner.Api/Services/AmtPowerService.cs index 2e7a5a4..f5b75eb 100644 --- a/backend-csharp/AmtScanner.Api/Services/AmtPowerService.cs +++ b/backend-csharp/AmtScanner.Api/Services/AmtPowerService.cs @@ -125,8 +125,13 @@ public class AmtPowerService : IAmtPowerService var returnValue = outputObject.GetProperty("ReturnValue"); var returnCode = Convert.ToUInt32(returnValue.ToString()); + _logger.LogInformation("电源操作返回码: {ReturnCode} for {Ip}", returnCode, ipAddress); - if (returnCode == 0) + // AMT 返回码说明: + // 0 = 成功完成 + // 4096 = 作业已启动(异步操作,也是成功) + // 2 = 操作不允许 + if (returnCode == 0 || returnCode == 4096) { result.Success = true; result.Message = GetActionSuccessMessage(action); diff --git a/backend-csharp/AmtScanner.Api/Services/WindowsScannerService.cs b/backend-csharp/AmtScanner.Api/Services/WindowsScannerService.cs index 30458cb..8879bae 100644 --- a/backend-csharp/AmtScanner.Api/Services/WindowsScannerService.cs +++ b/backend-csharp/AmtScanner.Api/Services/WindowsScannerService.cs @@ -11,11 +11,14 @@ namespace AmtScanner.Api.Services; public interface IWindowsScannerService { Task> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask, - IProgress progress, CancellationToken cancellationToken = default); + Action progressCallback, CancellationToken cancellationToken = default); Task GetOsInfoAsync(string ipAddress, string username, string password); Task GetSystemUuidAsync(string ipAddress, string username, string password); Task BindAmtDevicesAsync(); void CancelScan(string taskId); + List GetScanResults(string taskId); + void ClearScanResults(string taskId); + Task SaveSelectedDevicesAsync(string taskId, List selectedIps); } public class WindowsScannerService : IWindowsScannerService @@ -24,6 +27,8 @@ public class WindowsScannerService : IWindowsScannerService private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _cancellationTokens = new(); + // 存储扫描发现的设备(未保存到数据库) + private static readonly ConcurrentDictionary> _scanResults = new(); public WindowsScannerService( IServiceScopeFactory scopeFactory, @@ -39,17 +44,21 @@ public class WindowsScannerService : IWindowsScannerService string taskId, string networkSegment, string subnetMask, - IProgress progress, + Action progressCallback, CancellationToken cancellationToken = default) { - _logger.LogInformation("Starting OS scan for task: {TaskId}", taskId); + _logger.LogInformation("Starting OS scan for task: {TaskId}, network: {Network}{Mask}", + taskId, networkSegment, subnetMask); var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _cancellationTokens[taskId] = cts; + _scanResults[taskId] = new List(); try { var ipList = CalculateIpRange(networkSegment, subnetMask); + _logger.LogInformation("Calculated {Count} IPs to scan", ipList.Count); + var foundDevices = new ConcurrentBag(); int scannedCount = 0; int foundCount = 0; @@ -72,9 +81,18 @@ public class WindowsScannerService : IWindowsScannerService { foundDevices.Add(device); var found = Interlocked.Increment(ref foundCount); - await SaveOsDeviceAsync(device); + _logger.LogInformation("Found device: {Ip}, OS: {OsType}", ip, device.OsType); + + // 存储到内存中等待用户选择 + lock (_scanResults) + { + if (_scanResults.TryGetValue(taskId, out var list)) + { + list.Add(device); + } + } - progress.Report(new OsScanProgress + progressCallback(new OsScanProgress { TaskId = taskId, ScannedCount = scanned, @@ -87,7 +105,7 @@ public class WindowsScannerService : IWindowsScannerService } else { - progress.Report(new OsScanProgress + progressCallback(new OsScanProgress { TaskId = taskId, ScannedCount = scanned, @@ -104,9 +122,7 @@ public class WindowsScannerService : IWindowsScannerService } }); - // 扫描完成后尝试绑定 AMT 设备 - await BindAmtDevicesAsync(); - + _logger.LogInformation("OS scan completed. Found {Count} devices", foundDevices.Count); return foundDevices.ToList(); } finally @@ -116,6 +132,56 @@ public class WindowsScannerService : IWindowsScannerService } } + /// + /// 获取扫描结果(未保存的设备列表) + /// + public List GetScanResults(string taskId) + { + _logger.LogInformation("GetScanResults called for task: {TaskId}, available tasks: {Tasks}", + taskId, string.Join(", ", _scanResults.Keys)); + + if (_scanResults.TryGetValue(taskId, out var devices)) + { + _logger.LogInformation("Found {Count} devices for task: {TaskId}", devices.Count, taskId); + return devices.ToList(); + } + _logger.LogWarning("No scan results found for task: {TaskId}", taskId); + return new List(); + } + + /// + /// 清除扫描结果 + /// + public void ClearScanResults(string taskId) + { + _scanResults.TryRemove(taskId, out _); + } + + /// + /// 保存选中的设备到数据库 + /// + public async Task SaveSelectedDevicesAsync(string taskId, List selectedIps) + { + if (!_scanResults.TryGetValue(taskId, out var devices)) + { + return 0; + } + + var selectedDevices = devices.Where(d => selectedIps.Contains(d.IpAddress)).ToList(); + int savedCount = 0; + + foreach (var device in selectedDevices) + { + await SaveOsDeviceAsync(device); + savedCount++; + } + + // 保存后尝试绑定 AMT 设备 + await BindAmtDevicesAsync(); + + return savedCount; + } + public void CancelScan(string taskId) { if (_cancellationTokens.TryGetValue(taskId, out var cts)) diff --git a/backend-csharp/AmtScanner.Api/add_os_device_credentials.sql b/backend-csharp/AmtScanner.Api/add_os_device_credentials.sql new file mode 100644 index 0000000..a332241 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/add_os_device_credentials.sql @@ -0,0 +1,4 @@ +-- 为 OsDevices 表添加 Windows 凭据字段 +ALTER TABLE `OsDevices` +ADD COLUMN `WindowsUsername` VARCHAR(100) NULL AFTER `Description`, +ADD COLUMN `WindowsPassword` VARCHAR(500) NULL AFTER `WindowsUsername`; diff --git a/backend-csharp/AmtScanner.Api/update_desktop_menu.sql b/backend-csharp/AmtScanner.Api/update_desktop_menu.sql new file mode 100644 index 0000000..758715f --- /dev/null +++ b/backend-csharp/AmtScanner.Api/update_desktop_menu.sql @@ -0,0 +1,25 @@ +-- 更新桌面管理菜单结构 +-- 将原来的"远程桌面"改为"系统添加"和"系统管理"两个子菜单 + +-- 1. 更新原有的菜单项(ID=21)为"系统添加" +UPDATE Menus SET + Name = 'OsDevices', + Path = 'os-devices', + Component = '/desktop-manage/os-devices', + Title = '系统添加', + Sort = 1 +WHERE Id = 21; + +-- 2. 添加新的"系统管理"菜单项(ID=22) +INSERT INTO Menus (Id, ParentId, Name, Path, Component, Title, Icon, Sort, Roles, IsHide, KeepAlive, IsSystem, IsIframe, IsHideTab, CreatedAt) +VALUES (22, 20, 'DesktopDevices', 'devices', '/desktop-manage/devices', '系统管理', NULL, 2, 'R_SUPER,R_ADMIN', 0, 1, 1, 0, 0, NOW()) +ON DUPLICATE KEY UPDATE + Name = 'DesktopDevices', + Path = 'devices', + Component = '/desktop-manage/devices', + Title = '系统管理', + Sort = 2; + +-- 3. 为超级管理员和管理员角色添加新菜单权限 +INSERT IGNORE INTO RoleMenus (RoleId, MenuId) +SELECT r.Id, 22 FROM Roles r WHERE r.RoleCode IN ('R_SUPER', 'R_ADMIN');