From ca7231ecb9c2adf57561557e7ff999fc1718fb06 Mon Sep 17 00:00:00 2001 From: lvfengfree Date: Wed, 21 Jan 2026 19:53:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E8=BF=9B=E5=BA=A6=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E4=BC=98=E5=8C=96AMT=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E5=88=97=E8=A1=A8UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSystem/src/api/amt.ts | 11 +- adminSystem/src/views/amt/credentials.vue | 174 ---- adminSystem/src/views/amt/devices.vue | 255 ++++-- adminSystem/src/views/amt/scan.vue | 8 +- .../src/views/amt/windows-credentials.vue | 201 ----- .../Controllers/DevicesController.cs | 64 +- .../Controllers/ScanController.cs | 44 +- .../AmtScanner.Api/Data/AppDbContext.cs | 1 + .../AmtScanner.Api/Data/DbSeeder.cs | 10 +- ...121095352_AddDeviceReservation.Designer.cs | 801 ++++++++++++++++++ .../20260121095352_AddDeviceReservation.cs | 73 ++ .../Migrations/AppDbContextModelSnapshot.cs | 60 ++ .../AmtScanner.Api/Models/AmtDevice.cs | 10 + .../Services/AmtScannerService.cs | 10 +- .../Services/GuacamoleService.cs | 14 +- .../Services/IAmtScannerService.cs | 2 +- .../add_amt_credentials_columns.sql | 11 + .../AmtScanner.Api/delete_computer_use.sql | 5 + guacamole/README.md | 18 + guacamole/docker-compose.yml | 3 + 20 files changed, 1298 insertions(+), 477 deletions(-) delete mode 100644 adminSystem/src/views/amt/credentials.vue delete mode 100644 adminSystem/src/views/amt/windows-credentials.vue create mode 100644 backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.Designer.cs create mode 100644 backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.cs create mode 100644 backend-csharp/AmtScanner.Api/add_amt_credentials_columns.sql create mode 100644 backend-csharp/AmtScanner.Api/delete_computer_use.sql diff --git a/adminSystem/src/api/amt.ts b/adminSystem/src/api/amt.ts index 7149fe7..f64f57d 100644 --- a/adminSystem/src/api/amt.ts +++ b/adminSystem/src/api/amt.ts @@ -6,7 +6,7 @@ export const scanApi = { startScan(networkSegment: string, subnetMask: string) { return request.post({ url: '/api/scan/start', - params: { networkSegment, subnetMask } + data: { networkSegment, subnetMask } }) }, @@ -96,6 +96,15 @@ export const deviceApi = { return request.get({ url: `/api/devices/${id}/credentials` }) + }, + + // 设置设备 AMT 凭据 + setAmtCredentials(id: number, data: { username: string; password: string }) { + return request.put({ + url: `/api/devices/${id}/amt-credentials`, + data: data, + showSuccessMessage: true + }) } } diff --git a/adminSystem/src/views/amt/credentials.vue b/adminSystem/src/views/amt/credentials.vue deleted file mode 100644 index 52347ce..0000000 --- a/adminSystem/src/views/amt/credentials.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - - - diff --git a/adminSystem/src/views/amt/devices.vue b/adminSystem/src/views/amt/devices.vue index 18d9d78..aa3a781 100644 --- a/adminSystem/src/views/amt/devices.vue +++ b/adminSystem/src/views/amt/devices.vue @@ -25,10 +25,52 @@ + + +
+
+ + 已选择 {{ selectedDevices.length }} 台设备 + + 请勾选设备进行操作 +
+
+ + + 配置AMT账号 + + + + + 电源管理 + + + + + + 删除 + +
+
- + + - + - + - + @@ -83,34 +124,11 @@ {{ formatDateTime(row.discoveredAt) }} - + @@ -122,17 +140,23 @@ - - - - - + + +
+ + 将为以下 {{ credentialsTargetDevices.length }} 台设备配置相同的 AMT 账号: +
{{ credentialsTargetDevices.map(d => d.ipAddress).join(', ') }}
+
+
+ + + - + - + + - - diff --git a/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs b/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs index 030b098..2b986d8 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/DevicesController.cs @@ -29,10 +29,32 @@ public class DevicesController : ControllerBase } [HttpGet] - public async Task>>> GetAllDevices() + public async Task>>> GetAllDevices() { var devices = await _context.AmtDevices.ToListAsync(); - return Ok(ApiResponse>.Success(devices)); + + // 解密 AMT 密码返回给前端 + var result = devices.Select(d => new { + d.Id, + d.IpAddress, + d.Hostname, + d.SystemUuid, + d.MajorVersion, + d.MinorVersion, + d.ProvisioningState, + d.Description, + d.AmtOnline, + d.OsOnline, + d.WindowsUsername, + d.WindowsPassword, + d.AmtUsername, + AmtPasswordDecrypted = string.IsNullOrEmpty(d.AmtPassword) ? null : + System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(d.AmtPassword)), + d.DiscoveredAt, + d.LastSeenAt + }).ToList(); + + return Ok(ApiResponse>.Success(result.Cast().ToList())); } [HttpGet("{id}")] @@ -184,6 +206,35 @@ public class DevicesController : ControllerBase })); } + /// + /// 设置设备的 AMT 登录凭据 + /// + [HttpPut("{id}/amt-credentials")] + public async Task>> SetAmtCredentials(long id, [FromBody] SetAmtCredentialsRequest request) + { + var device = await _context.AmtDevices.FindAsync(id); + + if (device == null) + { + return Ok(ApiResponse.Fail(404, "设备不存在")); + } + + if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password)) + { + return Ok(ApiResponse.Fail(400, "用户名和密码不能为空")); + } + + device.AmtUsername = request.Username; + // 简单加密存储密码(生产环境应使用更安全的加密方式) + device.AmtPassword = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.Password)); + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Updated AMT credentials for device {Id} ({Ip})", id, device.IpAddress); + + return Ok(ApiResponse.Success(null, "AMT凭据设置成功")); + } + /// /// 检测所有设备的在线状态 /// @@ -407,3 +458,12 @@ public class AddDeviceRequest public string? WindowsUsername { get; set; } public string? WindowsPassword { get; set; } } + +/// +/// 设置设备 AMT 凭据请求 +/// +public class SetAmtCredentialsRequest +{ + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs b/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs index 04be814..ad1f499 100644 --- a/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs +++ b/backend-csharp/AmtScanner.Api/Controllers/ScanController.cs @@ -44,40 +44,46 @@ public class ScanController : ControllerBase FoundDevices = 0 }; + // 创建进度回调 - 直接使用 Action 而不是 Progress + Action progressCallback = p => + { + // 更新状态存储 + if (_scanStatuses.TryGetValue(taskId, out var status)) + { + status.ScannedCount = p.ScannedCount; + status.TotalCount = p.TotalCount; + status.FoundDevices = p.FoundDevices; + status.CurrentIp = p.CurrentIp; + + _logger.LogInformation("Progress update: scanned={Scanned}, total={Total}, found={Found}, ip={Ip}", + p.ScannedCount, p.TotalCount, p.FoundDevices, p.CurrentIp); + } + + // 异步发送 SignalR 通知(不等待) + _ = _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p); + }; + // Start scan in background _ = Task.Run(async () => { - var progress = new Progress(async p => - { - // 更新状态存储 - if (_scanStatuses.TryGetValue(taskId, out var status)) - { - status.ScannedCount = p.ScannedCount; - status.TotalCount = p.TotalCount; - status.FoundDevices = p.FoundDevices; - status.CurrentIp = p.CurrentIp; - } - - await _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p); - }); - try { - await _scannerService.ScanNetworkAsync( + var foundDevicesList = await _scannerService.ScanNetworkAsync( taskId, request.NetworkSegment, request.SubnetMask, - progress + progressCallback ); - // 更新状态为完成 + // 更新状态为完成,并确保 foundDevices 是正确的 if (_scanStatuses.TryGetValue(taskId, out var status)) { status.Status = "completed"; + status.FoundDevices = foundDevicesList.Count; + _logger.LogInformation("Scan task {TaskId} completed with {Count} devices found", taskId, foundDevicesList.Count); } // Send completion notification - _logger.LogInformation("Scan task {TaskId} completed", taskId); await _hubContext.Clients.All.SendAsync("ScanCompleted", new { taskId }); } catch (Exception ex) @@ -102,6 +108,8 @@ public class ScanController : ControllerBase { if (_scanStatuses.TryGetValue(taskId, out var status)) { + _logger.LogDebug("GetScanStatus: taskId={TaskId}, status={Status}, scanned={Scanned}, total={Total}, found={Found}", + taskId, status.Status, status.ScannedCount, status.TotalCount, status.FoundDevices); return Ok(ApiResponse.Success(status)); } diff --git a/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs b/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs index cab9d6b..de4b197 100644 --- a/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs +++ b/backend-csharp/AmtScanner.Api/Data/AppDbContext.cs @@ -192,5 +192,6 @@ public class AppDbContext : DbContext modelBuilder.Entity() .HasIndex(d => d.SystemUuid); + } } diff --git a/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs b/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs index 92e6d28..a9c82ba 100644 --- a/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs +++ b/backend-csharp/AmtScanner.Api/Data/DbSeeder.cs @@ -97,7 +97,10 @@ public static class DbSeeder private static async Task SeedMenusAsync(AppDbContext context) { - if (await context.Menus.AnyAsync()) return; + if (await context.Menus.AnyAsync()) + { + return; + } var menus = new List { @@ -106,13 +109,12 @@ public static class DbSeeder new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true }, // AMT 设备管理菜单(系统内置) - new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "AMT设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "AMT设备管理", Icon = "ri:computer-line", Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "设备扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备管理", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, - new() { Id = 8, ParentId = 5, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, // 桌面管理菜单(系统内置) - new() { Id = 20, Name = "DesktopManage", Path = "/desktop-manage", Component = "/index/index", Title = "桌面管理", Icon = "ri:remote-control-line", Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, + 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 }, // 系统管理菜单(系统内置) diff --git a/backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.Designer.cs b/backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.Designer.cs new file mode 100644 index 0000000..2602074 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.Designer.cs @@ -0,0 +1,801 @@ +// +using System; +using AmtScanner.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AmtScanner.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260121095352_AddDeviceReservation")] + partial class AddDeviceReservation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("AmtScanner.Api.Models.AmtCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AmtCredentials"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.AmtDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AmtOnline") + .HasColumnType("tinyint(1)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DiscoveredAt") + .HasColumnType("datetime(6)"); + + b.Property("Hostname") + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastSeenAt") + .HasColumnType("datetime(6)"); + + b.Property("MajorVersion") + .HasColumnType("int"); + + b.Property("MinorVersion") + .HasColumnType("int"); + + b.Property("OsOnline") + .HasColumnType("tinyint(1)"); + + b.Property("ProvisioningState") + .HasColumnType("int"); + + b.Property("SystemUuid") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("WindowsPassword") + .HasColumnType("longtext"); + + b.Property("WindowsUsername") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("IpAddress") + .IsUnique(); + + b.HasIndex("SystemUuid"); + + b.ToTable("AmtDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Status", "EndTime"); + + b.ToTable("DeviceReservations"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetime(6)"); + + b.Property("ProcessorCores") + .HasColumnType("int"); + + b.Property("ProcessorCurrentClockSpeed") + .HasColumnType("int"); + + b.Property("ProcessorMaxClockSpeed") + .HasColumnType("int"); + + b.Property("ProcessorModel") + .HasColumnType("longtext"); + + b.Property("ProcessorThreads") + .HasColumnType("int"); + + b.Property("SystemManufacturer") + .HasColumnType("longtext"); + + b.Property("SystemModel") + .HasColumnType("longtext"); + + b.Property("SystemSerialNumber") + .HasColumnType("longtext"); + + b.Property("SystemUuid") + .HasColumnType("longtext"); + + b.Property("TotalMemoryBytes") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("LastUpdated"); + + b.ToTable("HardwareInfos"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CapacityBytes") + .HasColumnType("bigint"); + + b.Property("HardwareInfoId") + .HasColumnType("bigint"); + + b.Property("Manufacturer") + .HasColumnType("longtext"); + + b.Property("MemoryType") + .HasColumnType("longtext"); + + b.Property("PartNumber") + .HasColumnType("longtext"); + + b.Property("SerialNumber") + .HasColumnType("longtext"); + + b.Property("SlotLocation") + .HasColumnType("longtext"); + + b.Property("SpeedMHz") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("HardwareInfoId"); + + b.ToTable("MemoryModules"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Menu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AuthList") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Component") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("IsHide") + .HasColumnType("tinyint(1)"); + + b.Property("IsHideTab") + .HasColumnType("tinyint(1)"); + + b.Property("IsIframe") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("KeepAlive") + .HasColumnType("tinyint(1)"); + + b.Property("Link") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ParentId") + .HasColumnType("int"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Roles") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Sort") + .HasColumnType("int"); + + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Menus"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.OsDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AmtDeviceId") + .HasColumnType("bigint"); + + b.Property("Architecture") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DiscoveredAt") + .HasColumnType("datetime(6)"); + + b.Property("Hostname") + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("IsOnline") + .HasColumnType("tinyint(1)"); + + b.Property("LastBootTime") + .HasColumnType("datetime(6)"); + + b.Property("LastOnlineAt") + .HasColumnType("datetime(6)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LoggedInUser") + .HasColumnType("longtext"); + + b.Property("MacAddress") + .HasColumnType("longtext"); + + b.Property("OsType") + .HasColumnType("int"); + + b.Property("OsVersion") + .HasColumnType("longtext"); + + b.Property("SystemUuid") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AmtDeviceId"); + + b.HasIndex("IpAddress") + .IsUnique(); + + b.HasIndex("SystemUuid"); + + b.ToTable("OsDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("MaxUseCount") + .HasColumnType("int"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UseCount") + .HasColumnType("int"); + + b.Property("UsedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("RemoteAccessTokens"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("RoleCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("RoleCode") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b => + { + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("MenuId") + .HasColumnType("int"); + + b.HasKey("RoleId", "MenuId"); + + b.HasIndex("MenuId"); + + b.ToTable("RoleMenus"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CapacityBytes") + .HasColumnType("bigint"); + + b.Property("DeviceId") + .HasColumnType("longtext"); + + b.Property("HardwareInfoId") + .HasColumnType("bigint"); + + b.Property("InterfaceType") + .HasColumnType("longtext"); + + b.Property("Model") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("HardwareInfoId"); + + b.ToTable("StorageDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Avatar") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("varchar(1)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("NickName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("RefreshToken") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("varchar(1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Domain") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("WindowsCredentials"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AmtScanner.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b => + { + b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo") + .WithMany("MemoryModules") + .HasForeignKey("HardwareInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HardwareInfo"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Menu", b => + { + b.HasOne("AmtScanner.Api.Models.Menu", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.OsDevice", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "AmtDevice") + .WithMany() + .HasForeignKey("AmtDeviceId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AmtDevice"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b => + { + b.HasOne("AmtScanner.Api.Models.Menu", "Menu") + .WithMany("RoleMenus") + .HasForeignKey("MenuId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AmtScanner.Api.Models.Role", "Role") + .WithMany("RoleMenus") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Menu"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b => + { + b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo") + .WithMany("StorageDevices") + .HasForeignKey("HardwareInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HardwareInfo"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b => + { + b.HasOne("AmtScanner.Api.Models.Role", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AmtScanner.Api.Models.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => + { + b.Navigation("MemoryModules"); + + b.Navigation("StorageDevices"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Menu", b => + { + b.Navigation("Children"); + + b.Navigation("RoleMenus"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.Role", b => + { + b.Navigation("RoleMenus"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("AmtScanner.Api.Models.User", b => + { + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.cs b/backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.cs new file mode 100644 index 0000000..7d144f5 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AmtScanner.Api.Migrations +{ + /// + public partial class AddDeviceReservation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DeviceReservations", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + DeviceId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "int", nullable: false), + StartTime = table.Column(type: "datetime(6)", nullable: false), + EndTime = table.Column(type: "datetime(6)", nullable: false), + Status = table.Column(type: "int", nullable: false), + AccessToken = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Note = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceReservations", x => x.Id); + table.ForeignKey( + name: "FK_DeviceReservations_AmtDevices_DeviceId", + column: x => x.DeviceId, + principalTable: "AmtDevices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DeviceReservations_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceReservations_DeviceId", + table: "DeviceReservations", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceReservations_DeviceId_Status_EndTime", + table: "DeviceReservations", + columns: new[] { "DeviceId", "Status", "EndTime" }); + + migrationBuilder.CreateIndex( + name: "IX_DeviceReservations_UserId", + table: "DeviceReservations", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DeviceReservations"); + } + } +} diff --git a/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs b/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs index ded95d3..07adf9d 100644 --- a/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs +++ b/backend-csharp/AmtScanner.Api/Migrations/AppDbContextModelSnapshot.cs @@ -115,6 +115,47 @@ namespace AmtScanner.Api.Migrations b.ToTable("AmtDevices"); }); + modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Status", "EndTime"); + + b.ToTable("DeviceReservations"); + }); + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => { b.Property("Id") @@ -605,6 +646,25 @@ namespace AmtScanner.Api.Migrations b.ToTable("WindowsCredentials"); }); + modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b => + { + b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AmtScanner.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b => { b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") diff --git a/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs b/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs index 4e2be76..3367781 100644 --- a/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs +++ b/backend-csharp/AmtScanner.Api/Models/AmtDevice.cs @@ -45,6 +45,16 @@ public class AmtDevice /// public string? WindowsPassword { get; set; } + /// + /// AMT 登录用户名 + /// + public string? AmtUsername { get; set; } + + /// + /// AMT 登录密码(加密存储) + /// + public string? AmtPassword { get; set; } + public DateTime DiscoveredAt { get; set; } public DateTime LastSeenAt { get; set; } diff --git a/backend-csharp/AmtScanner.Api/Services/AmtScannerService.cs b/backend-csharp/AmtScanner.Api/Services/AmtScannerService.cs index 5e5d183..76bd191 100644 --- a/backend-csharp/AmtScanner.Api/Services/AmtScannerService.cs +++ b/backend-csharp/AmtScanner.Api/Services/AmtScannerService.cs @@ -32,7 +32,7 @@ public class AmtScannerService : IAmtScannerService string taskId, string networkSegment, string subnetMask, - IProgress progress, + Action progressCallback, CancellationToken cancellationToken = default) { _logger.LogInformation("Starting network scan for task: {TaskId}", taskId); @@ -69,10 +69,13 @@ public class AmtScannerService : IAmtScannerService foundDevices.Add(device); var found = Interlocked.Increment(ref foundCount); + _logger.LogInformation("Found AMT device at {Ip}, total found: {Found}", ip, found); + // Save to database await SaveDeviceAsync(device); - progress.Report(new ScanProgress + // 直接调用回调,不使用 Progress + progressCallback(new ScanProgress { TaskId = taskId, ScannedCount = scanned, @@ -85,7 +88,8 @@ public class AmtScannerService : IAmtScannerService } else { - progress.Report(new ScanProgress + // 直接调用回调,不使用 Progress + progressCallback(new ScanProgress { TaskId = taskId, ScannedCount = scanned, diff --git a/backend-csharp/AmtScanner.Api/Services/GuacamoleService.cs b/backend-csharp/AmtScanner.Api/Services/GuacamoleService.cs index c23bc44..e60ba0a 100644 --- a/backend-csharp/AmtScanner.Api/Services/GuacamoleService.cs +++ b/backend-csharp/AmtScanner.Api/Services/GuacamoleService.cs @@ -104,7 +104,12 @@ public class GuacamoleService : IGuacamoleService ["enable-menu-animations"] = "true", ["disable-bitmap-caching"] = "false", ["disable-offscreen-caching"] = "false", - ["disable-glyph-caching"] = "false" + ["disable-glyph-caching"] = "false", + // 启用驱动器重定向(文件传输) + ["enable-drive"] = "true", + ["drive-name"] = "共享文件夹", + ["drive-path"] = "/drive", + ["create-drive-path"] = "true" }, ["attributes"] = new Dictionary() }; @@ -204,7 +209,12 @@ public class GuacamoleService : IGuacamoleService ["enable-font-smoothing"] = "true", ["enable-full-window-drag"] = "true", ["enable-desktop-composition"] = "true", - ["enable-menu-animations"] = "true" + ["enable-menu-animations"] = "true", + // 启用驱动器重定向(文件传输) + ["enable-drive"] = "true", + ["drive-name"] = "共享文件夹", + ["drive-path"] = "/drive", + ["create-drive-path"] = "true" }, ["attributes"] = new Dictionary() }; diff --git a/backend-csharp/AmtScanner.Api/Services/IAmtScannerService.cs b/backend-csharp/AmtScanner.Api/Services/IAmtScannerService.cs index 457a38a..ef7c851 100644 --- a/backend-csharp/AmtScanner.Api/Services/IAmtScannerService.cs +++ b/backend-csharp/AmtScanner.Api/Services/IAmtScannerService.cs @@ -4,7 +4,7 @@ namespace AmtScanner.Api.Services; public interface IAmtScannerService { - Task> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask, IProgress progress, CancellationToken cancellationToken = default); + Task> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask, Action progressCallback, CancellationToken cancellationToken = default); void CancelScan(string taskId); } diff --git a/backend-csharp/AmtScanner.Api/add_amt_credentials_columns.sql b/backend-csharp/AmtScanner.Api/add_amt_credentials_columns.sql new file mode 100644 index 0000000..267f0dc --- /dev/null +++ b/backend-csharp/AmtScanner.Api/add_amt_credentials_columns.sql @@ -0,0 +1,11 @@ +-- 为 AmtDevices 表添加 AMT 登录凭据字段 +-- 执行前请确保已备份数据库 + +-- 添加 AmtUsername 字段 +ALTER TABLE `AmtDevices` ADD COLUMN `AmtUsername` VARCHAR(100) NULL AFTER `WindowsPassword`; + +-- 添加 AmtPassword 字段(加密存储) +ALTER TABLE `AmtDevices` ADD COLUMN `AmtPassword` VARCHAR(500) NULL AFTER `AmtUsername`; + +-- 验证字段是否添加成功 +-- SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'AmtDevices' AND COLUMN_NAME IN ('AmtUsername', 'AmtPassword'); diff --git a/backend-csharp/AmtScanner.Api/delete_computer_use.sql b/backend-csharp/AmtScanner.Api/delete_computer_use.sql new file mode 100644 index 0000000..58079b2 --- /dev/null +++ b/backend-csharp/AmtScanner.Api/delete_computer_use.sql @@ -0,0 +1,5 @@ +-- 删除"电脑使用"菜单 +DELETE FROM Menus WHERE Path = '/computer-use'; + +-- 删除预约表(如果存在) +DROP TABLE IF EXISTS DeviceReservations; diff --git a/guacamole/README.md b/guacamole/README.md index 5661489..65f3e7b 100644 --- a/guacamole/README.md +++ b/guacamole/README.md @@ -210,3 +210,21 @@ ports: 2. 修改数据库默认密码 3. 生产环境使用 HTTPS(配置 Nginx 反向代理) 4. 限制访问 IP 范围 + +## 文件传输功能 + +系统已启用 Guacamole 的驱动器重定向功能,可以在远程桌面中传输文件。 + +### 使用方法 + +1. 连接远程桌面后,打开 **文件资源管理器** +2. 在左侧导航栏找到 **网络位置** 或 **此电脑** +3. 会看到一个名为 **共享文件夹** 的网络驱动器 +4. 将文件拖放到该驱动器即可上传,从该驱动器复制文件即可下载 + +### 注意事项 + +- 共享文件夹存储在 Docker 卷 `guacd-drive` 中 +- 所有连接共享同一个文件夹,请注意文件管理 +- 大文件传输可能较慢,取决于网络带宽 +- 如需为每个用户/连接分配独立文件夹,需要进一步配置 diff --git a/guacamole/docker-compose.yml b/guacamole/docker-compose.yml index 3239c9a..74f6a85 100644 --- a/guacamole/docker-compose.yml +++ b/guacamole/docker-compose.yml @@ -6,6 +6,8 @@ services: image: guacamole/guacd:latest container_name: guacd restart: always + volumes: + - guacd-drive:/drive networks: - guacamole-net @@ -50,3 +52,4 @@ networks: volumes: guacamole-db-data: + guacd-drive: