feat: 实现OS设备扫描和UUID绑定功能

- 添加OsDevice模型和OsDevicesController
- 实现WindowsScannerService用于网络扫描和WMI查询
- 添加AMT设备UUID查询功能(从CIM_ComputerSystemPackage获取PlatformGUID)
- 实现PlatformGUID到标准UUID格式的转换(字节序转换)
- 修复HardwareInfoRepository保存UUID的问题
- 前端添加OS设备管理页面和UUID获取/刷新按钮
- 添加数据库迁移脚本
This commit is contained in:
lvfengfree 2026-01-21 16:16:48 +08:00
parent c546d4635a
commit eebbacafde
22 changed files with 3259 additions and 3 deletions

View File

@ -311,3 +311,83 @@ export const remoteDesktopApi = {
}) })
} }
} }
// 操作系统设备 API
export const osDeviceApi = {
// 获取所有操作系统设备
getAll() {
return request.get<any[]>({
url: '/api/os-devices'
})
},
// 获取单个设备
getById(id: number) {
return request.get({
url: `/api/os-devices/${id}`
})
},
// 启动操作系统扫描
startScan(networkSegment: string, subnetMask: string) {
return request.post({
url: '/api/os-devices/scan/start',
params: { networkSegment, subnetMask }
})
},
// 获取扫描状态
getScanStatus(taskId: string) {
return request.get({
url: `/api/os-devices/scan/status/${taskId}`
})
},
// 取消扫描
cancelScan(taskId: string) {
return request.post({
url: `/api/os-devices/scan/cancel/${taskId}`
})
},
// 获取设备详细信息(通过 WMI
fetchInfo(id: number, credentials: { username: string; password: string }) {
return request.post({
url: `/api/os-devices/${id}/fetch-info`,
data: credentials,
showSuccessMessage: true
})
},
// 手动绑定 AMT 设备
bindAmt(id: number, amtDeviceId: number) {
return request.post({
url: `/api/os-devices/${id}/bind-amt/${amtDeviceId}`,
showSuccessMessage: true
})
},
// 解除 AMT 绑定
unbindAmt(id: number) {
return request.post({
url: `/api/os-devices/${id}/unbind-amt`,
showSuccessMessage: true
})
},
// 自动绑定所有设备
autoBind() {
return request.post({
url: '/api/os-devices/auto-bind',
showSuccessMessage: true
})
},
// 删除设备
delete(id: number) {
return request.del({
url: `/api/os-devices/${id}`,
showSuccessMessage: true
})
}
}

View File

@ -28,6 +28,23 @@
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%"> <ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" /> <ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn prop="systemUuid" label="系统 UUID" width="360">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 8px;">
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 12px;">{{ row.systemUuid }}</span>
<ElTag v-else type="info" size="small">未获取</ElTag>
<ElButton
type="primary"
size="small"
link
@click="handleFetchUuid(row)"
:loading="row.fetchingUuid"
>
{{ row.systemUuid ? '刷新' : '获取' }}
</ElButton>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="AMT 版本" width="100"> <ElTableColumn label="AMT 版本" width="100">
<template #default="{ row }"> <template #default="{ row }">
{{ row.majorVersion }}.{{ row.minorVersion }} {{ row.majorVersion }}.{{ row.minorVersion }}
@ -130,7 +147,7 @@
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose } from '@element-plus/icons-vue' import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose } from '@element-plus/icons-vue'
import { deviceApi, powerApi } from '@/api/amt' import { deviceApi, powerApi, hardwareApi } from '@/api/amt'
import HardwareInfoModal from './modules/hardware-info-modal.vue' import HardwareInfoModal from './modules/hardware-info-modal.vue'
import RemoteDesktopModal from './modules/remote-desktop-modal.vue' import RemoteDesktopModal from './modules/remote-desktop-modal.vue'
@ -291,6 +308,24 @@ const saveCredentials = async () => {
} }
} }
const handleFetchUuid = async (device: any) => {
device.fetchingUuid = true
try {
// UUID
const hardwareInfo = await hardwareApi.getHardwareInfo(device.id, true)
if (hardwareInfo.systemInfo?.uuid) {
device.systemUuid = hardwareInfo.systemInfo.uuid
ElMessage.success('UUID 获取成功')
} else {
ElMessage.warning('未能从设备获取 UUID')
}
} catch (error: any) {
ElMessage.error('获取 UUID 失败: ' + (error.message || '未知错误'))
} finally {
device.fetchingUuid = false
}
}
const handlePowerCommand = async (command: string, device: any) => { const handlePowerCommand = async (command: string, device: any) => {
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = { const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' }, 'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },

View File

@ -29,9 +29,12 @@
<ElDescriptionsItem label="型号" v-if="hardwareInfo.systemInfo?.model"> <ElDescriptionsItem label="型号" v-if="hardwareInfo.systemInfo?.model">
{{ hardwareInfo.systemInfo.model }} {{ hardwareInfo.systemInfo.model }}
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="序列号" v-if="hardwareInfo.systemInfo?.serialNumber" :span="2"> <ElDescriptionsItem label="序列号" v-if="hardwareInfo.systemInfo?.serialNumber">
{{ hardwareInfo.systemInfo.serialNumber }} {{ hardwareInfo.systemInfo.serialNumber }}
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="系统 UUID" v-if="hardwareInfo.systemInfo?.uuid">
{{ hardwareInfo.systemInfo.uuid }}
</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
<!-- CPU 信息 --> <!-- CPU 信息 -->

View File

@ -0,0 +1,330 @@
<template>
<div class="os-devices-page">
<!-- 工具栏 -->
<div class="toolbar">
<div class="left">
<ElButton type="primary" @click="showScanDialog = true">
<ElIcon><Search /></ElIcon>
扫描操作系统
</ElButton>
<ElButton @click="handleAutoBind" :loading="autoBinding">
<ElIcon><Link /></ElIcon>
自动绑定 AMT
</ElButton>
<ElButton @click="loadDevices" :loading="loading">
<ElIcon><Refresh /></ElIcon>
刷新
</ElButton>
</div>
<div class="right">
<ElInput v-model="searchKeyword" placeholder="搜索 IP/主机名" clearable style="width: 200px" />
</div>
</div>
<!-- 设备列表 -->
<ElTable :data="filteredDevices" v-loading="loading" stripe>
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn prop="hostname" label="主机名" width="150" />
<ElTableColumn label="操作系统" width="200">
<template #default="{ row }">
<ElTag :type="getOsTagType(row.osType)" size="small">{{ row.osType }}</ElTag>
<span v-if="row.osVersion" style="margin-left: 5px; font-size: 12px; color: #999">
{{ row.osVersion?.substring(0, 30) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn prop="systemUuid" label="UUID" width="280">
<template #default="{ row }">
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 11px">{{ row.systemUuid }}</span>
<ElTag v-else type="warning" size="small">未获取</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="AMT 绑定" width="150">
<template #default="{ row }">
<ElTag v-if="row.amtDeviceId" type="success" size="small">
{{ row.amtDeviceIp }}
</ElTag>
<ElTag v-else type="info" size="small">未绑定</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="状态" width="80">
<template #default="{ row }">
<ElTag :type="row.isOnline ? 'success' : 'danger'" size="small">
{{ row.isOnline ? '在线' : '离线' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="280" fixed="right">
<template #default="{ row }">
<ElButton size="small" @click="handleFetchInfo(row)">获取信息</ElButton>
<ElButton size="small" @click="handleBindAmt(row)" v-if="!row.amtDeviceId">绑定 AMT</ElButton>
<ElButton size="small" type="warning" @click="handleUnbindAmt(row)" v-else>解绑</ElButton>
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
</template>
</ElTableColumn>
</ElTable>
<!-- 扫描对话框 -->
<ElDialog v-model="showScanDialog" title="扫描操作系统" width="500px">
<ElForm :model="scanForm" label-width="100px">
<ElFormItem label="网段">
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
</ElFormItem>
<ElFormItem label="子网掩码">
<ElSelect v-model="scanForm.subnetMask" style="width: 100%">
<ElOption label="/24 (255.255.255.0)" value="/24" />
<ElOption label="/16 (255.255.0.0)" value="/16" />
<ElOption label="/8 (255.0.0.0)" value="/8" />
</ElSelect>
</ElFormItem>
</ElForm>
<div v-if="scanning" class="scan-progress">
<ElProgress :percentage="scanProgress.progressPercentage" :format="() => `${scanProgress.scannedCount}/${scanProgress.totalCount}`" />
<p>当前扫描: {{ scanProgress.currentIp }}</p>
<p>已发现: {{ scanProgress.foundDevices }} 台设备</p>
</div>
<template #footer>
<ElButton @click="showScanDialog = false" :disabled="scanning">取消</ElButton>
<ElButton type="primary" @click="startScan" :loading="scanning">
{{ scanning ? '扫描中...' : '开始扫描' }}
</ElButton>
</template>
</ElDialog>
<!-- 获取信息对话框 -->
<ElDialog v-model="showFetchDialog" title="获取系统信息" width="400px">
<ElAlert type="info" :closable="false" style="margin-bottom: 15px">
需要提供 Windows 管理员凭据通过 WMI 获取系统信息
</ElAlert>
<ElForm :model="fetchForm" label-width="80px">
<ElFormItem label="用户名">
<ElInput v-model="fetchForm.username" placeholder="administrator" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="fetchForm.password" type="password" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showFetchDialog = false">取消</ElButton>
<ElButton type="primary" @click="doFetchInfo" :loading="fetching">获取</ElButton>
</template>
</ElDialog>
<!-- 绑定 AMT 对话框 -->
<ElDialog v-model="showBindDialog" title="绑定 AMT 设备" width="500px">
<ElTable :data="amtDevices" v-loading="loadingAmt" @row-click="selectAmtDevice" highlight-current-row>
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn prop="hostname" label="主机名" />
<ElTableColumn prop="systemUuid" label="UUID" width="280">
<template #default="{ row }">
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 11px">{{ row.systemUuid }}</span>
<ElTag v-else type="warning" size="small">未获取</ElTag>
</template>
</ElTableColumn>
</ElTable>
<template #footer>
<ElButton @click="showBindDialog = false">取消</ElButton>
<ElButton type="primary" @click="doBind" :disabled="!selectedAmtDevice">绑定</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Link } from '@element-plus/icons-vue'
import { osDeviceApi, deviceApi } from '@/api/amt'
const loading = ref(false)
const devices = ref<any[]>([])
const searchKeyword = ref('')
const autoBinding = ref(false)
//
const showScanDialog = ref(false)
const scanning = ref(false)
const scanForm = ref({ networkSegment: '192.168.1.0', subnetMask: '/24' })
const scanProgress = ref({ scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' })
let scanTaskId = ''
//
const showFetchDialog = ref(false)
const fetching = ref(false)
const fetchForm = ref({ username: '', password: '' })
let currentFetchDevice: any = null
//
const showBindDialog = ref(false)
const loadingAmt = ref(false)
const amtDevices = ref<any[]>([])
const selectedAmtDevice = ref<any>(null)
let currentBindDevice: any = null
const filteredDevices = computed(() => {
if (!searchKeyword.value) return devices.value
const kw = searchKeyword.value.toLowerCase()
return devices.value.filter(d =>
d.ipAddress?.toLowerCase().includes(kw) ||
d.hostname?.toLowerCase().includes(kw)
)
})
const getOsTagType = (osType: string) => {
switch (osType) {
case 'Windows': return 'primary'
case 'Linux': return 'success'
default: return 'info'
}
}
const loadDevices = async () => {
loading.value = true
try {
devices.value = await osDeviceApi.getAll()
} catch (error) {
console.error('加载设备失败', error)
} finally {
loading.value = false
}
}
const startScan = async () => {
scanning.value = true
scanProgress.value = { scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' }
try {
const result = await osDeviceApi.startScan(scanForm.value.networkSegment, scanForm.value.subnetMask)
scanTaskId = result.taskId
let retryCount = 0
const maxRetries = 3
//
const pollProgress = async () => {
if (!scanning.value) return
try {
const progress = await osDeviceApi.getScanStatus(scanTaskId)
retryCount = 0 //
scanProgress.value = progress
if (progress.progressPercentage < 100) {
setTimeout(pollProgress, 500)
} else {
scanning.value = false
showScanDialog.value = false
ElMessage.success(`扫描完成,发现 ${progress.foundDevices} 台设备`)
loadDevices()
}
} catch (error) {
retryCount++
if (retryCount < maxRetries) {
//
setTimeout(pollProgress, 1000)
} else {
scanning.value = false
ElMessage.error('获取扫描进度失败')
}
}
}
// 500ms
setTimeout(pollProgress, 500)
} catch (error) {
scanning.value = false
ElMessage.error('启动扫描失败')
}
}
const handleAutoBind = async () => {
autoBinding.value = true
try {
await osDeviceApi.autoBind()
loadDevices()
} finally {
autoBinding.value = false
}
}
const handleFetchInfo = (row: any) => {
currentFetchDevice = row
fetchForm.value = { username: '', password: '' }
showFetchDialog.value = true
}
const doFetchInfo = async () => {
if (!fetchForm.value.username || !fetchForm.value.password) {
ElMessage.warning('请输入凭据')
return
}
fetching.value = true
try {
await osDeviceApi.fetchInfo(currentFetchDevice.id, fetchForm.value)
showFetchDialog.value = false
loadDevices()
} catch (error: any) {
ElMessage.error(error.message || '获取信息失败')
} finally {
fetching.value = false
}
}
const handleBindAmt = async (row: any) => {
currentBindDevice = row
selectedAmtDevice.value = null
showBindDialog.value = true
loadingAmt.value = true
try {
amtDevices.value = await deviceApi.getAllDevices()
} finally {
loadingAmt.value = false
}
}
const selectAmtDevice = (row: any) => {
selectedAmtDevice.value = row
}
const doBind = async () => {
if (!selectedAmtDevice.value) return
try {
await osDeviceApi.bindAmt(currentBindDevice.id, selectedAmtDevice.value.id)
showBindDialog.value = false
loadDevices()
} catch (error) {
ElMessage.error('绑定失败')
}
}
const handleUnbindAmt = async (row: any) => {
try {
await ElMessageBox.confirm('确定要解除 AMT 绑定吗?', '确认')
await osDeviceApi.unbindAmt(row.id)
loadDevices()
} catch (error) {
if (error !== 'cancel') ElMessage.error('解绑失败')
}
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm('确定要删除此设备吗?', '确认删除', { type: 'warning' })
await osDeviceApi.delete(row.id)
loadDevices()
} catch (error) {
if (error !== 'cancel') ElMessage.error('删除失败')
}
}
onMounted(() => {
loadDevices()
})
</script>
<style scoped>
.os-devices-page { padding: 20px; }
.toolbar { display: flex; justify-content: space-between; margin-bottom: 20px; }
.toolbar .left { display: flex; gap: 10px; }
.scan-progress { margin-top: 20px; text-align: center; }
.scan-progress p { margin: 10px 0; color: #666; }
</style>

View File

@ -16,6 +16,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.Management" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,323 @@
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,
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");
var progress = new Progress<OsScanProgress>(p =>
{
_scanProgress[taskId] = p;
});
_ = Task.Run(async () =>
{
try
{
await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progress);
}
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));
}
return Ok(ApiResponse<OsScanProgress>.Fail(404, "扫描任务不存在"));
}
/// <summary>
/// 取消扫描
/// </summary>
[HttpPost("scan/cancel/{taskId}")]
public ActionResult<ApiResponse<object>> CancelScan(string taskId)
{
_scannerService.CancelScan(taskId);
return Ok(ApiResponse<object>.Success(null, "扫描已取消"));
}
/// <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>
/// 删除设备
/// </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 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;
}

View File

@ -17,6 +17,7 @@ public class AppDbContext : DbContext
public DbSet<StorageDevice> StorageDevices { get; set; } public DbSet<StorageDevice> StorageDevices { get; set; }
public DbSet<WindowsCredential> WindowsCredentials { get; set; } public DbSet<WindowsCredential> WindowsCredentials { get; set; }
public DbSet<RemoteAccessToken> RemoteAccessTokens { get; set; } public DbSet<RemoteAccessToken> RemoteAccessTokens { get; set; }
public DbSet<OsDevice> OsDevices { get; set; }
// 用户认证相关 // 用户认证相关
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
@ -161,5 +162,35 @@ public class AppDbContext : DbContext
.WithMany(m => m.RoleMenus) .WithMany(m => m.RoleMenus)
.HasForeignKey(rm => rm.MenuId) .HasForeignKey(rm => rm.MenuId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
// OsDevice 配置
modelBuilder.Entity<OsDevice>()
.Property(o => o.IpAddress)
.HasMaxLength(50);
modelBuilder.Entity<OsDevice>()
.HasIndex(o => o.IpAddress)
.IsUnique();
modelBuilder.Entity<OsDevice>()
.Property(o => o.SystemUuid)
.HasMaxLength(50);
modelBuilder.Entity<OsDevice>()
.HasIndex(o => o.SystemUuid);
modelBuilder.Entity<OsDevice>()
.HasOne(o => o.AmtDevice)
.WithMany()
.HasForeignKey(o => o.AmtDeviceId)
.OnDelete(DeleteBehavior.SetNull);
// AmtDevice SystemUuid 索引
modelBuilder.Entity<AmtDevice>()
.Property(d => d.SystemUuid)
.HasMaxLength(50);
modelBuilder.Entity<AmtDevice>()
.HasIndex(d => d.SystemUuid);
} }
} }

View File

@ -0,0 +1,738 @@
// <auto-generated />
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("20260121062236_AddOsDeviceAndSystemUuid")]
partial class AddOsDeviceAndSystemUuid
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<bool>("IsDefault")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Name");
b.ToTable("AmtCredentials");
});
modelBuilder.Entity("AmtScanner.Api.Models.AmtDevice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<bool>("AmtOnline")
.HasColumnType("tinyint(1)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<DateTime>("DiscoveredAt")
.HasColumnType("datetime(6)");
b.Property<string>("Hostname")
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)");
b.Property<int>("MajorVersion")
.HasColumnType("int");
b.Property<int>("MinorVersion")
.HasColumnType("int");
b.Property<bool>("OsOnline")
.HasColumnType("tinyint(1)");
b.Property<int>("ProvisioningState")
.HasColumnType("int");
b.Property<string>("SystemUuid")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("WindowsPassword")
.HasColumnType("longtext");
b.Property<string>("WindowsUsername")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("IpAddress")
.IsUnique();
b.HasIndex("SystemUuid");
b.ToTable("AmtDevices");
});
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime(6)");
b.Property<int?>("ProcessorCores")
.HasColumnType("int");
b.Property<int?>("ProcessorCurrentClockSpeed")
.HasColumnType("int");
b.Property<int?>("ProcessorMaxClockSpeed")
.HasColumnType("int");
b.Property<string>("ProcessorModel")
.HasColumnType("longtext");
b.Property<int?>("ProcessorThreads")
.HasColumnType("int");
b.Property<string>("SystemManufacturer")
.HasColumnType("longtext");
b.Property<string>("SystemModel")
.HasColumnType("longtext");
b.Property<string>("SystemSerialNumber")
.HasColumnType("longtext");
b.Property<long?>("TotalMemoryBytes")
.HasColumnType("bigint");
b.Property<DateTime>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("CapacityBytes")
.HasColumnType("bigint");
b.Property<long>("HardwareInfoId")
.HasColumnType("bigint");
b.Property<string>("Manufacturer")
.HasColumnType("longtext");
b.Property<string>("MemoryType")
.HasColumnType("longtext");
b.Property<string>("PartNumber")
.HasColumnType("longtext");
b.Property<string>("SerialNumber")
.HasColumnType("longtext");
b.Property<string>("SlotLocation")
.HasColumnType("longtext");
b.Property<int?>("SpeedMHz")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("HardwareInfoId");
b.ToTable("MemoryModules");
});
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AuthList")
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("Component")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Icon")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<bool>("IsHide")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsHideTab")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsIframe")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<bool>("KeepAlive")
.HasColumnType("tinyint(1)");
b.Property<string>("Link")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Roles")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<int>("Sort")
.HasColumnType("int");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("AmtDeviceId")
.HasColumnType("bigint");
b.Property<string>("Architecture")
.HasColumnType("longtext");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<DateTime>("DiscoveredAt")
.HasColumnType("datetime(6)");
b.Property<string>("Hostname")
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<bool>("IsOnline")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastBootTime")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastOnlineAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("LoggedInUser")
.HasColumnType("longtext");
b.Property<string>("MacAddress")
.HasColumnType("longtext");
b.Property<int>("OsType")
.HasColumnType("int");
b.Property<string>("OsVersion")
.HasColumnType("longtext");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsUsed")
.HasColumnType("tinyint(1)");
b.Property<int>("MaxUseCount")
.HasColumnType("int");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<int>("UseCount")
.HasColumnType("int");
b.Property<DateTime?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<bool>("Enabled")
.HasColumnType("tinyint(1)");
b.Property<string>("RoleCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("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<int>("RoleId")
.HasColumnType("int");
b.Property<int>("MenuId")
.HasColumnType("int");
b.HasKey("RoleId", "MenuId");
b.HasIndex("MenuId");
b.ToTable("RoleMenus");
});
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("CapacityBytes")
.HasColumnType("bigint");
b.Property<string>("DeviceId")
.HasColumnType("longtext");
b.Property<long>("HardwareInfoId")
.HasColumnType("bigint");
b.Property<string>("InterfaceType")
.HasColumnType("longtext");
b.Property<string>("Model")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("HardwareInfoId");
b.ToTable("StorageDevices");
});
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Avatar")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Gender")
.IsRequired()
.HasMaxLength(1)
.HasColumnType("varchar(1)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<string>("NickName")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("RefreshToken")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime?>("RefreshTokenExpiryTime")
.HasColumnType("datetime(6)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(1)
.HasColumnType("varchar(1)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("UpdatedBy")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("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<int>("UserId")
.HasColumnType("int");
b.Property<int>("RoleId")
.HasColumnType("int");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles");
});
modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Domain")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<bool>("IsDefault")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.HasKey("Id");
b.HasIndex("Name");
b.ToTable("WindowsCredentials");
});
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
}
}
}

View File

@ -0,0 +1,102 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AmtScanner.Api.Migrations
{
/// <inheritdoc />
public partial class AddOsDeviceAndSystemUuid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SystemUuid",
table: "AmtDevices",
type: "varchar(50)",
maxLength: 50,
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "OsDevices",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
IpAddress = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SystemUuid = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Hostname = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
OsType = table.Column<int>(type: "int", nullable: false),
OsVersion = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Architecture = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
LoggedInUser = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
LastBootTime = table.Column<DateTime>(type: "datetime(6)", nullable: true),
MacAddress = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IsOnline = table.Column<bool>(type: "tinyint(1)", nullable: false),
LastOnlineAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
DiscoveredAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
LastUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Description = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
AmtDeviceId = table.Column<long>(type: "bigint", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OsDevices", x => x.Id);
table.ForeignKey(
name: "FK_OsDevices_AmtDevices_AmtDeviceId",
column: x => x.AmtDeviceId,
principalTable: "AmtDevices",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_AmtDevices_SystemUuid",
table: "AmtDevices",
column: "SystemUuid");
migrationBuilder.CreateIndex(
name: "IX_OsDevices_AmtDeviceId",
table: "OsDevices",
column: "AmtDeviceId");
migrationBuilder.CreateIndex(
name: "IX_OsDevices_IpAddress",
table: "OsDevices",
column: "IpAddress",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OsDevices_SystemUuid",
table: "OsDevices",
column: "SystemUuid");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OsDevices");
migrationBuilder.DropIndex(
name: "IX_AmtDevices_SystemUuid",
table: "AmtDevices");
migrationBuilder.DropColumn(
name: "SystemUuid",
table: "AmtDevices");
}
}
}

View File

@ -0,0 +1,741 @@
// <auto-generated />
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("20260121071149_AddSystemUuidToHardwareInfo")]
partial class AddSystemUuidToHardwareInfo
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<bool>("IsDefault")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Name");
b.ToTable("AmtCredentials");
});
modelBuilder.Entity("AmtScanner.Api.Models.AmtDevice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<bool>("AmtOnline")
.HasColumnType("tinyint(1)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<DateTime>("DiscoveredAt")
.HasColumnType("datetime(6)");
b.Property<string>("Hostname")
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)");
b.Property<int>("MajorVersion")
.HasColumnType("int");
b.Property<int>("MinorVersion")
.HasColumnType("int");
b.Property<bool>("OsOnline")
.HasColumnType("tinyint(1)");
b.Property<int>("ProvisioningState")
.HasColumnType("int");
b.Property<string>("SystemUuid")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("WindowsPassword")
.HasColumnType("longtext");
b.Property<string>("WindowsUsername")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("IpAddress")
.IsUnique();
b.HasIndex("SystemUuid");
b.ToTable("AmtDevices");
});
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<DateTime>("LastUpdated")
.HasColumnType("datetime(6)");
b.Property<int?>("ProcessorCores")
.HasColumnType("int");
b.Property<int?>("ProcessorCurrentClockSpeed")
.HasColumnType("int");
b.Property<int?>("ProcessorMaxClockSpeed")
.HasColumnType("int");
b.Property<string>("ProcessorModel")
.HasColumnType("longtext");
b.Property<int?>("ProcessorThreads")
.HasColumnType("int");
b.Property<string>("SystemManufacturer")
.HasColumnType("longtext");
b.Property<string>("SystemModel")
.HasColumnType("longtext");
b.Property<string>("SystemSerialNumber")
.HasColumnType("longtext");
b.Property<string>("SystemUuid")
.HasColumnType("longtext");
b.Property<long?>("TotalMemoryBytes")
.HasColumnType("bigint");
b.Property<DateTime>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("CapacityBytes")
.HasColumnType("bigint");
b.Property<long>("HardwareInfoId")
.HasColumnType("bigint");
b.Property<string>("Manufacturer")
.HasColumnType("longtext");
b.Property<string>("MemoryType")
.HasColumnType("longtext");
b.Property<string>("PartNumber")
.HasColumnType("longtext");
b.Property<string>("SerialNumber")
.HasColumnType("longtext");
b.Property<string>("SlotLocation")
.HasColumnType("longtext");
b.Property<int?>("SpeedMHz")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("HardwareInfoId");
b.ToTable("MemoryModules");
});
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AuthList")
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("Component")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Icon")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<bool>("IsHide")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsHideTab")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsIframe")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<bool>("KeepAlive")
.HasColumnType("tinyint(1)");
b.Property<string>("Link")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Roles")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<int>("Sort")
.HasColumnType("int");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("AmtDeviceId")
.HasColumnType("bigint");
b.Property<string>("Architecture")
.HasColumnType("longtext");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<DateTime>("DiscoveredAt")
.HasColumnType("datetime(6)");
b.Property<string>("Hostname")
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<bool>("IsOnline")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastBootTime")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastOnlineAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("LoggedInUser")
.HasColumnType("longtext");
b.Property<string>("MacAddress")
.HasColumnType("longtext");
b.Property<int>("OsType")
.HasColumnType("int");
b.Property<string>("OsVersion")
.HasColumnType("longtext");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("DeviceId")
.HasColumnType("bigint");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsUsed")
.HasColumnType("tinyint(1)");
b.Property<int>("MaxUseCount")
.HasColumnType("int");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<int>("UseCount")
.HasColumnType("int");
b.Property<DateTime?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<bool>("Enabled")
.HasColumnType("tinyint(1)");
b.Property<string>("RoleCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("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<int>("RoleId")
.HasColumnType("int");
b.Property<int>("MenuId")
.HasColumnType("int");
b.HasKey("RoleId", "MenuId");
b.HasIndex("MenuId");
b.ToTable("RoleMenus");
});
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("CapacityBytes")
.HasColumnType("bigint");
b.Property<string>("DeviceId")
.HasColumnType("longtext");
b.Property<long>("HardwareInfoId")
.HasColumnType("bigint");
b.Property<string>("InterfaceType")
.HasColumnType("longtext");
b.Property<string>("Model")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("HardwareInfoId");
b.ToTable("StorageDevices");
});
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Avatar")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Gender")
.IsRequired()
.HasMaxLength(1)
.HasColumnType("varchar(1)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<string>("NickName")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("RefreshToken")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime?>("RefreshTokenExpiryTime")
.HasColumnType("datetime(6)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(1)
.HasColumnType("varchar(1)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("UpdatedBy")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("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<int>("UserId")
.HasColumnType("int");
b.Property<int>("RoleId")
.HasColumnType("int");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles");
});
modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Domain")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<bool>("IsDefault")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.HasKey("Id");
b.HasIndex("Name");
b.ToTable("WindowsCredentials");
});
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
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AmtScanner.Api.Migrations
{
/// <inheritdoc />
public partial class AddSystemUuidToHardwareInfo : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SystemUuid",
table: "HardwareInfos",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SystemUuid",
table: "HardwareInfos");
}
}
}

View File

@ -95,6 +95,10 @@ namespace AmtScanner.Api.Migrations
b.Property<int>("ProvisioningState") b.Property<int>("ProvisioningState")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("SystemUuid")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("WindowsPassword") b.Property<string>("WindowsPassword")
.HasColumnType("longtext"); .HasColumnType("longtext");
@ -106,6 +110,8 @@ namespace AmtScanner.Api.Migrations
b.HasIndex("IpAddress") b.HasIndex("IpAddress")
.IsUnique(); .IsUnique();
b.HasIndex("SystemUuid");
b.ToTable("AmtDevices"); b.ToTable("AmtDevices");
}); });
@ -148,6 +154,9 @@ namespace AmtScanner.Api.Migrations
b.Property<string>("SystemSerialNumber") b.Property<string>("SystemSerialNumber")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("SystemUuid")
.HasColumnType("longtext");
b.Property<long?>("TotalMemoryBytes") b.Property<long?>("TotalMemoryBytes")
.HasColumnType("bigint"); .HasColumnType("bigint");
@ -273,6 +282,72 @@ namespace AmtScanner.Api.Migrations
b.ToTable("Menus"); b.ToTable("Menus");
}); });
modelBuilder.Entity("AmtScanner.Api.Models.OsDevice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<long?>("AmtDeviceId")
.HasColumnType("bigint");
b.Property<string>("Architecture")
.HasColumnType("longtext");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<DateTime>("DiscoveredAt")
.HasColumnType("datetime(6)");
b.Property<string>("Hostname")
.HasColumnType("longtext");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<bool>("IsOnline")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastBootTime")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastOnlineAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("LoggedInUser")
.HasColumnType("longtext");
b.Property<string>("MacAddress")
.HasColumnType("longtext");
b.Property<int>("OsType")
.HasColumnType("int");
b.Property<string>("OsVersion")
.HasColumnType("longtext");
b.Property<string>("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 => modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@ -562,6 +637,16 @@ namespace AmtScanner.Api.Migrations
b.Navigation("Parent"); 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 => modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
{ {
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device") b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")

View File

@ -12,6 +12,11 @@ public class AmtDevice
public string? Hostname { get; set; } public string? Hostname { get; set; }
/// <summary>
/// 系统 UUIDSMBIOS UUID用于与操作系统绑定
/// </summary>
public string? SystemUuid { get; set; }
public int MajorVersion { get; set; } public int MajorVersion { get; set; }
public int MinorVersion { get; set; } public int MinorVersion { get; set; }

View File

@ -19,6 +19,11 @@ public class HardwareInfo
public string? SystemModel { get; set; } public string? SystemModel { get; set; }
public string? SystemSerialNumber { get; set; } public string? SystemSerialNumber { get; set; }
/// <summary>
/// 系统 UUIDSMBIOS UUID用于与操作系统绑定
/// </summary>
public string? SystemUuid { get; set; }
// Processor Information // Processor Information
public string? ProcessorModel { get; set; } public string? ProcessorModel { get; set; }
public int? ProcessorCores { get; set; } public int? ProcessorCores { get; set; }

View File

@ -16,6 +16,7 @@ public class SystemInfoDto
public string? Manufacturer { get; set; } public string? Manufacturer { get; set; }
public string? Model { get; set; } public string? Model { get; set; }
public string? SerialNumber { get; set; } public string? SerialNumber { get; set; }
public string? Uuid { get; set; }
} }
public class ProcessorInfoDto public class ProcessorInfoDto

View File

@ -0,0 +1,103 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AmtScanner.Api.Models;
/// <summary>
/// 操作系统设备实体
/// </summary>
public class OsDevice
{
[Key]
public long Id { get; set; }
/// <summary>
/// IP 地址
/// </summary>
[Required]
public string IpAddress { get; set; } = string.Empty;
/// <summary>
/// 系统 UUIDSMBIOS UUID用于与 AMT 绑定)
/// </summary>
public string? SystemUuid { get; set; }
/// <summary>
/// 主机名
/// </summary>
public string? Hostname { get; set; }
/// <summary>
/// 操作系统类型
/// </summary>
public OsType OsType { get; set; } = OsType.Unknown;
/// <summary>
/// 操作系统版本
/// </summary>
public string? OsVersion { get; set; }
/// <summary>
/// 操作系统架构
/// </summary>
public string? Architecture { get; set; }
/// <summary>
/// 当前登录用户
/// </summary>
public string? LoggedInUser { get; set; }
/// <summary>
/// 系统最后启动时间
/// </summary>
public DateTime? LastBootTime { get; set; }
/// <summary>
/// MAC 地址
/// </summary>
public string? MacAddress { get; set; }
/// <summary>
/// 是否在线
/// </summary>
public bool IsOnline { get; set; }
/// <summary>
/// 最后在线时间
/// </summary>
public DateTime? LastOnlineAt { get; set; }
/// <summary>
/// 发现时间
/// </summary>
public DateTime DiscoveredAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// 最后更新时间
/// </summary>
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// 备注
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 关联的 AMT 设备 ID
/// </summary>
public long? AmtDeviceId { get; set; }
/// <summary>
/// 关联的 AMT 设备
/// </summary>
[ForeignKey(nameof(AmtDeviceId))]
public AmtDevice? AmtDevice { get; set; }
}
public enum OsType
{
Unknown = 0,
Windows = 1,
Linux = 2,
MacOS = 3
}

View File

@ -60,6 +60,7 @@ builder.Services.AddScoped<IAmtHardwareQueryService, AmtHardwareQueryService>();
builder.Services.AddScoped<IHardwareInfoService, HardwareInfoService>(); builder.Services.AddScoped<IHardwareInfoService, HardwareInfoService>();
builder.Services.AddScoped<IHardwareInfoRepository, HardwareInfoRepository>(); builder.Services.AddScoped<IHardwareInfoRepository, HardwareInfoRepository>();
builder.Services.AddScoped<IAmtPowerService, AmtPowerService>(); builder.Services.AddScoped<IAmtPowerService, AmtPowerService>();
builder.Services.AddScoped<IWindowsScannerService, WindowsScannerService>();
builder.Services.AddHttpClient<IGuacamoleService, GuacamoleService>(); builder.Services.AddHttpClient<IGuacamoleService, GuacamoleService>();
// Add JWT Configuration // Add JWT Configuration

View File

@ -44,6 +44,7 @@ public class HardwareInfoRepository : IHardwareInfoRepository
existing.SystemManufacturer = hardwareInfo.SystemManufacturer; existing.SystemManufacturer = hardwareInfo.SystemManufacturer;
existing.SystemModel = hardwareInfo.SystemModel; existing.SystemModel = hardwareInfo.SystemModel;
existing.SystemSerialNumber = hardwareInfo.SystemSerialNumber; existing.SystemSerialNumber = hardwareInfo.SystemSerialNumber;
existing.SystemUuid = hardwareInfo.SystemUuid; // 保存 UUID
existing.ProcessorModel = hardwareInfo.ProcessorModel; existing.ProcessorModel = hardwareInfo.ProcessorModel;
existing.ProcessorCores = hardwareInfo.ProcessorCores; existing.ProcessorCores = hardwareInfo.ProcessorCores;
existing.ProcessorThreads = hardwareInfo.ProcessorThreads; existing.ProcessorThreads = hardwareInfo.ProcessorThreads;

View File

@ -152,6 +152,9 @@ public class AmtHardwareQueryService : IAmtHardwareQueryService
_logger.LogDebug("System info: {Manufacturer} {Model}", _logger.LogDebug("System info: {Manufacturer} {Model}",
hardwareInfo.SystemManufacturer, hardwareInfo.SystemModel); hardwareInfo.SystemManufacturer, hardwareInfo.SystemModel);
// Query UUID from CIM_ComputerSystemPackage (在同一个 try 块内查询,确保连接有效)
QuerySystemUuid(connection, hardwareInfo);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -159,6 +162,159 @@ public class AmtHardwareQueryService : IAmtHardwareQueryService
} }
} }
private void QuerySystemUuid(IWsmanConnection connection, HardwareInfo hardwareInfo)
{
try
{
_logger.LogInformation("Querying system UUID");
// 通过 CIM_ComputerSystemPackage 获取 PlatformGUID (UUID)
var query = connection.ExecQuery("SELECT * FROM CIM_ComputerSystemPackage");
foreach (IWsmanItem item in query)
{
try
{
// 尝试获取 PlatformGUID
var platformGuid = item.Object.GetProperty("PlatformGUID");
_logger.LogInformation("PlatformGUID IsNull: {IsNull}, Value: {Value}",
platformGuid.IsNull, platformGuid.IsNull ? "null" : platformGuid.ToString());
if (!platformGuid.IsNull && !string.IsNullOrWhiteSpace(platformGuid.ToString()))
{
var rawGuid = platformGuid.ToString().Trim();
// 将 PlatformGUID 转换为标准 UUID 格式
var formattedUuid = FormatPlatformGuidToUuid(rawGuid);
hardwareInfo.SystemUuid = formattedUuid;
_logger.LogInformation("Found UUID from CIM_ComputerSystemPackage.PlatformGUID: Raw={Raw}, Formatted={Formatted}",
rawGuid, formattedUuid);
return;
}
}
catch (Exception ex)
{
_logger.LogInformation(ex, "PlatformGUID not available from CIM_ComputerSystemPackage");
}
// 备选:尝试从 Antecedent (CIM_Chassis) 获取 UUID
try
{
var antecedent = item.Object.GetProperty("Antecedent");
_logger.LogInformation("Antecedent IsNull: {IsNull}, IsA CIM_Chassis: {IsChassis}",
antecedent.IsNull, !antecedent.IsNull && antecedent.IsA("CIM_Chassis"));
if (!antecedent.IsNull && antecedent.IsA("CIM_Chassis"))
{
var chassisObj = antecedent.Ref.Get();
var uuid = chassisObj.GetProperty("UUID");
_logger.LogInformation("CIM_Chassis UUID IsNull: {IsNull}, Value: {Value}",
uuid.IsNull, uuid.IsNull ? "null" : uuid.ToString());
if (!uuid.IsNull && !string.IsNullOrWhiteSpace(uuid.ToString()))
{
hardwareInfo.SystemUuid = uuid.ToString().Trim();
_logger.LogInformation("Found UUID from CIM_Chassis: {Uuid}", hardwareInfo.SystemUuid);
return;
}
}
}
catch (Exception ex)
{
_logger.LogInformation(ex, "UUID not available from CIM_Chassis");
}
}
// 备选方案:尝试从 CIM_PhysicalPackage 获取
try
{
_logger.LogInformation("Trying CIM_PhysicalPackage for UUID");
var physicalQuery = connection.ExecQuery("SELECT * FROM CIM_PhysicalPackage");
foreach (IWsmanItem item in physicalQuery)
{
try
{
var uuid = item.Object.GetProperty("UUID");
_logger.LogInformation("CIM_PhysicalPackage UUID IsNull: {IsNull}, Value: {Value}",
uuid.IsNull, uuid.IsNull ? "null" : uuid.ToString());
if (!uuid.IsNull && !string.IsNullOrWhiteSpace(uuid.ToString()))
{
hardwareInfo.SystemUuid = uuid.ToString().Trim();
_logger.LogInformation("Found UUID from CIM_PhysicalPackage: {Uuid}", hardwareInfo.SystemUuid);
return;
}
}
catch (Exception ex)
{
_logger.LogInformation(ex, "UUID property not available from CIM_PhysicalPackage item");
}
}
}
catch (Exception ex)
{
_logger.LogInformation(ex, "Failed to query CIM_PhysicalPackage for UUID");
}
_logger.LogWarning("Could not find system UUID from any source");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to query system UUID");
}
}
/// <summary>
/// 将 AMT PlatformGUID 转换为标准 UUID 格式
/// PlatformGUID 是 32 位十六进制字符串,需要转换为 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 格式
///
/// 示例:
/// AMT PlatformGUID: B826D58CD3E2E31187820B47ABD01400
/// Windows UUID: 8CD526B8-E2D3-11E3-8782-0B47ABD01400
///
/// 转换规则前三组需要按字节反转每2个十六进制字符为1字节
/// </summary>
private string FormatPlatformGuidToUuid(string platformGuid)
{
// 移除可能存在的连字符
var cleanGuid = platformGuid.Replace("-", "").ToUpperInvariant();
if (cleanGuid.Length != 32)
{
_logger.LogWarning("Invalid PlatformGUID length: {Length}, expected 32", cleanGuid.Length);
return platformGuid; // 返回原始值
}
// AMT PlatformGUID 格式32字符
// B826D58C D3E2 E311 8782 0B47ABD01400
// 位置: 0-7 8-11 12-15 16-19 20-31
// 标准 UUID 格式:
// 8CD526B8-E2D3-11E3-8782-0B47ABD01400
// 第一组4 字节8字符按字节反转
// B826D58C -> 8C D5 26 B8 -> 8CD526B8
var part1 = cleanGuid.Substring(0, 8);
var part1Reversed = $"{part1[6]}{part1[7]}{part1[4]}{part1[5]}{part1[2]}{part1[3]}{part1[0]}{part1[1]}";
// 第二组2 字节4字符按字节反转
// D3E2 -> E2 D3 -> E2D3
var part2 = cleanGuid.Substring(8, 4);
var part2Reversed = $"{part2[2]}{part2[3]}{part2[0]}{part2[1]}";
// 第三组2 字节4字符按字节反转
// E311 -> 11 E3 -> 11E3
var part3 = cleanGuid.Substring(12, 4);
var part3Reversed = $"{part3[2]}{part3[3]}{part3[0]}{part3[1]}";
// 第四组2 字节4字符不反转
var part4 = cleanGuid.Substring(16, 4);
// 第五组6 字节12字符不反转
var part5 = cleanGuid.Substring(20, 12);
var result = $"{part1Reversed}-{part2Reversed}-{part3Reversed}-{part4}-{part5}";
_logger.LogInformation("UUID conversion: {Raw} -> {Formatted}", cleanGuid, result);
return result;
}
private void QueryProcessorInfo(IWsmanConnection connection, HardwareInfo hardwareInfo) private void QueryProcessorInfo(IWsmanConnection connection, HardwareInfo hardwareInfo)
{ {
try try

View File

@ -89,6 +89,15 @@ public class HardwareInfoService : IHardwareInfoService
hardwareInfo.DeviceId = deviceId; hardwareInfo.DeviceId = deviceId;
// 如果查询到了 UUID保存到 AmtDevice
if (!string.IsNullOrEmpty(hardwareInfo.SystemUuid) && device.SystemUuid != hardwareInfo.SystemUuid)
{
device.SystemUuid = hardwareInfo.SystemUuid;
context.AmtDevices.Update(device);
await context.SaveChangesAsync();
_logger.LogInformation("Updated device {DeviceId} with UUID: {Uuid}", deviceId, hardwareInfo.SystemUuid);
}
// Save to cache // Save to cache
await _repository.SaveAsync(hardwareInfo); await _repository.SaveAsync(hardwareInfo);
@ -149,7 +158,8 @@ public class HardwareInfoService : IHardwareInfoService
{ {
Manufacturer = hardwareInfo.SystemManufacturer, Manufacturer = hardwareInfo.SystemManufacturer,
Model = hardwareInfo.SystemModel, Model = hardwareInfo.SystemModel,
SerialNumber = hardwareInfo.SystemSerialNumber SerialNumber = hardwareInfo.SystemSerialNumber,
Uuid = hardwareInfo.SystemUuid
}, },
Processor = new ProcessorInfoDto Processor = new ProcessorInfoDto
{ {

View File

@ -0,0 +1,465 @@
using AmtScanner.Api.Data;
using AmtScanner.Api.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Concurrent;
using System.Management;
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace AmtScanner.Api.Services;
public interface IWindowsScannerService
{
Task<List<OsDevice>> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask,
IProgress<OsScanProgress> progress, CancellationToken cancellationToken = default);
Task<OsDevice?> GetOsInfoAsync(string ipAddress, string username, string password);
Task<string?> GetSystemUuidAsync(string ipAddress, string username, string password);
Task BindAmtDevicesAsync();
void CancelScan(string taskId);
}
public class WindowsScannerService : IWindowsScannerService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<WindowsScannerService> _logger;
private readonly IConfiguration _configuration;
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokens = new();
public WindowsScannerService(
IServiceScopeFactory scopeFactory,
ILogger<WindowsScannerService> logger,
IConfiguration configuration)
{
_scopeFactory = scopeFactory;
_logger = logger;
_configuration = configuration;
}
public async Task<List<OsDevice>> ScanNetworkAsync(
string taskId,
string networkSegment,
string subnetMask,
IProgress<OsScanProgress> progress,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Starting OS scan for task: {TaskId}", taskId);
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_cancellationTokens[taskId] = cts;
try
{
var ipList = CalculateIpRange(networkSegment, subnetMask);
var foundDevices = new ConcurrentBag<OsDevice>();
int scannedCount = 0;
int foundCount = 0;
var threadPoolSize = _configuration.GetValue<int>("Scanner:ThreadPoolSize", 50);
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = threadPoolSize,
CancellationToken = cts.Token
};
await Parallel.ForEachAsync(ipList, parallelOptions, async (ip, ct) =>
{
try
{
var device = await ScanSingleHostAsync(ip, ct);
var scanned = Interlocked.Increment(ref scannedCount);
if (device != null)
{
foundDevices.Add(device);
var found = Interlocked.Increment(ref foundCount);
await SaveOsDeviceAsync(device);
progress.Report(new OsScanProgress
{
TaskId = taskId,
ScannedCount = scanned,
TotalCount = ipList.Count,
FoundDevices = found,
ProgressPercentage = (double)scanned / ipList.Count * 100,
CurrentIp = ip,
LatestDevice = device
});
}
else
{
progress.Report(new OsScanProgress
{
TaskId = taskId,
ScannedCount = scanned,
TotalCount = ipList.Count,
FoundDevices = foundCount,
ProgressPercentage = (double)scanned / ipList.Count * 100,
CurrentIp = ip
});
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error scanning {Ip}", ip);
}
});
// 扫描完成后尝试绑定 AMT 设备
await BindAmtDevicesAsync();
return foundDevices.ToList();
}
finally
{
_cancellationTokens.TryRemove(taskId, out _);
cts.Dispose();
}
}
public void CancelScan(string taskId)
{
if (_cancellationTokens.TryGetValue(taskId, out var cts))
{
cts.Cancel();
_logger.LogInformation("OS scan task {TaskId} cancelled", taskId);
}
}
private async Task<OsDevice?> ScanSingleHostAsync(string ip, CancellationToken ct)
{
// 先 Ping 检测是否在线
if (!await IsHostOnlineAsync(ip, ct))
return null;
// 检测 Windows 端口
var isWindows = await IsWindowsHostAsync(ip, ct);
if (isWindows)
{
return new OsDevice
{
IpAddress = ip,
OsType = OsType.Windows,
IsOnline = true,
LastOnlineAt = DateTime.UtcNow,
DiscoveredAt = DateTime.UtcNow,
LastUpdatedAt = DateTime.UtcNow,
Description = "通过端口扫描发现"
};
}
// 检测 Linux (SSH 端口)
var isLinux = await IsPortOpenAsync(ip, 22, 2000, ct);
if (isLinux)
{
return new OsDevice
{
IpAddress = ip,
OsType = OsType.Linux,
IsOnline = true,
LastOnlineAt = DateTime.UtcNow,
DiscoveredAt = DateTime.UtcNow,
LastUpdatedAt = DateTime.UtcNow,
Description = "通过 SSH 端口发现"
};
}
return null;
}
private async Task<bool> IsHostOnlineAsync(string ip, CancellationToken ct)
{
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync(ip, 1000);
return reply.Status == IPStatus.Success;
}
catch
{
return false;
}
}
private async Task<bool> IsWindowsHostAsync(string ip, CancellationToken ct)
{
// 检测 Windows 常用端口: 135(RPC), 445(SMB), 3389(RDP), 5985(WinRM)
var windowsPorts = new[] { 135, 445, 3389, 5985 };
foreach (var port in windowsPorts)
{
if (await IsPortOpenAsync(ip, port, 1000, ct))
return true;
}
return false;
}
private async Task<bool> IsPortOpenAsync(string ip, int port, int timeoutMs, CancellationToken ct)
{
try
{
using var client = new TcpClient();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeoutMs);
await client.ConnectAsync(ip, port, cts.Token);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 通过 WMI 获取远程 Windows 系统信息
/// </summary>
public async Task<OsDevice?> GetOsInfoAsync(string ipAddress, string username, string password)
{
return await Task.Run(() =>
{
try
{
var options = new ConnectionOptions
{
Username = username,
Password = password,
Impersonation = ImpersonationLevel.Impersonate,
Authentication = AuthenticationLevel.PacketPrivacy
};
var scope = new ManagementScope($"\\\\{ipAddress}\\root\\cimv2", options);
scope.Connect();
var device = new OsDevice
{
IpAddress = ipAddress,
OsType = OsType.Windows,
IsOnline = true,
LastOnlineAt = DateTime.UtcNow,
LastUpdatedAt = DateTime.UtcNow
};
// 获取 UUID
var uuidQuery = new ObjectQuery("SELECT UUID FROM Win32_ComputerSystemProduct");
using (var uuidSearcher = new ManagementObjectSearcher(scope, uuidQuery))
{
foreach (var obj in uuidSearcher.Get())
{
device.SystemUuid = obj["UUID"]?.ToString();
break;
}
}
// 获取操作系统信息
var osQuery = new ObjectQuery("SELECT Caption, Version, OSArchitecture, LastBootUpTime FROM Win32_OperatingSystem");
using (var osSearcher = new ManagementObjectSearcher(scope, osQuery))
{
foreach (var obj in osSearcher.Get())
{
device.OsVersion = $"{obj["Caption"]} ({obj["Version"]})";
device.Architecture = obj["OSArchitecture"]?.ToString();
var lastBootStr = obj["LastBootUpTime"]?.ToString();
if (!string.IsNullOrEmpty(lastBootStr))
{
device.LastBootTime = ManagementDateTimeConverter.ToDateTime(lastBootStr);
}
break;
}
}
// 获取计算机名
var csQuery = new ObjectQuery("SELECT Name, UserName FROM Win32_ComputerSystem");
using (var csSearcher = new ManagementObjectSearcher(scope, csQuery))
{
foreach (var obj in csSearcher.Get())
{
device.Hostname = obj["Name"]?.ToString();
device.LoggedInUser = obj["UserName"]?.ToString();
break;
}
}
// 获取 MAC 地址
var netQuery = new ObjectQuery("SELECT MACAddress FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True");
using (var netSearcher = new ManagementObjectSearcher(scope, netQuery))
{
foreach (var obj in netSearcher.Get())
{
var mac = obj["MACAddress"]?.ToString();
if (!string.IsNullOrEmpty(mac))
{
device.MacAddress = mac;
break;
}
}
}
device.Description = "通过 WMI 获取详细信息";
return device;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get OS info for {Ip} via WMI", ipAddress);
return null;
}
});
}
/// <summary>
/// 获取远程 Windows 系统的 UUID
/// </summary>
public async Task<string?> GetSystemUuidAsync(string ipAddress, string username, string password)
{
return await Task.Run(() =>
{
try
{
var options = new ConnectionOptions
{
Username = username,
Password = password,
Impersonation = ImpersonationLevel.Impersonate,
Authentication = AuthenticationLevel.PacketPrivacy
};
var scope = new ManagementScope($"\\\\{ipAddress}\\root\\cimv2", options);
scope.Connect();
var query = new ObjectQuery("SELECT UUID FROM Win32_ComputerSystemProduct");
using var searcher = new ManagementObjectSearcher(scope, query);
foreach (var obj in searcher.Get())
{
return obj["UUID"]?.ToString();
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get UUID for {Ip}", ipAddress);
return null;
}
});
}
/// <summary>
/// 根据 UUID 自动绑定 AMT 设备和操作系统设备
/// </summary>
public async Task BindAmtDevicesAsync()
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 获取所有有 UUID 的操作系统设备
var osDevices = await context.OsDevices
.Where(o => o.SystemUuid != null && o.AmtDeviceId == null)
.ToListAsync();
// 获取所有有 UUID 的 AMT 设备
var amtDevices = await context.AmtDevices
.Where(a => a.SystemUuid != null)
.ToListAsync();
var amtUuidMap = amtDevices.ToDictionary(a => a.SystemUuid!, a => a);
foreach (var osDevice in osDevices)
{
if (osDevice.SystemUuid != null && amtUuidMap.TryGetValue(osDevice.SystemUuid, out var amtDevice))
{
osDevice.AmtDeviceId = amtDevice.Id;
_logger.LogInformation("Bound OS device {OsIp} to AMT device {AmtIp} via UUID {Uuid}",
osDevice.IpAddress, amtDevice.IpAddress, osDevice.SystemUuid);
}
}
await context.SaveChangesAsync();
}
private async Task SaveOsDeviceAsync(OsDevice device)
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var existing = await context.OsDevices
.FirstOrDefaultAsync(d => d.IpAddress == device.IpAddress);
if (existing != null)
{
existing.OsType = device.OsType;
existing.IsOnline = device.IsOnline;
existing.LastOnlineAt = device.LastOnlineAt;
existing.LastUpdatedAt = DateTime.UtcNow;
if (!string.IsNullOrEmpty(device.SystemUuid))
existing.SystemUuid = device.SystemUuid;
if (!string.IsNullOrEmpty(device.Hostname))
existing.Hostname = device.Hostname;
if (!string.IsNullOrEmpty(device.OsVersion))
existing.OsVersion = device.OsVersion;
}
else
{
context.OsDevices.Add(device);
}
await context.SaveChangesAsync();
}
private List<string> CalculateIpRange(string networkSegment, string subnetMask)
{
var ipList = new List<string>();
try
{
var networkLong = IpToLong(networkSegment);
var cidr = SubnetMaskToCidr(subnetMask);
var hostBits = 32 - cidr;
var totalHosts = (int)Math.Pow(2, hostBits);
for (int i = 1; i < totalHosts - 1; i++)
{
var ipLong = networkLong + i;
ipList.Add(LongToIp(ipLong));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating IP range");
}
return ipList;
}
private long IpToLong(string ipAddress)
{
var parts = ipAddress.Split('.');
long result = 0;
for (int i = 0; i < 4; i++)
result = result << 8 | long.Parse(parts[i]);
return result;
}
private string LongToIp(long ip) =>
$"{(ip >> 24) & 0xFF}.{(ip >> 16) & 0xFF}.{(ip >> 8) & 0xFF}.{ip & 0xFF}";
private int SubnetMaskToCidr(string subnetMask)
{
if (subnetMask.StartsWith("/"))
return int.Parse(subnetMask.Substring(1));
var parts = subnetMask.Split('.');
int cidr = 0;
foreach (var part in parts)
cidr += Convert.ToString(int.Parse(part), 2).Count(c => c == '1');
return cidr;
}
}
public class OsScanProgress
{
public string TaskId { get; set; } = string.Empty;
public int ScannedCount { get; set; }
public int TotalCount { get; set; }
public int FoundDevices { get; set; }
public double ProgressPercentage { get; set; }
public string? CurrentIp { get; set; }
public OsDevice? LatestDevice { get; set; }
}

View File

@ -0,0 +1,11 @@
-- 添加操作系统设备管理菜单放在桌面管理目录下ParentId=20
INSERT INTO `Menus` (`Id`, `Name`, `Title`, `Icon`, `Path`, `Component`, `ParentId`, `Sort`, `IsHide`, `KeepAlive`, `IsIframe`, `IsSystem`, `IsHideTab`, `CreatedAt`)
VALUES (22, 'OsDevices', '操作系统', 'Monitor', 'os-devices', '/desktop-manage/os-devices', 20, 2, 0, 0, 0, 0, 0, NOW());
-- 为超级管理员角色添加菜单权限
INSERT INTO `RoleMenus` (`RoleId`, `MenuId`)
SELECT r.Id, 22 FROM `Roles` r WHERE r.RoleCode = 'R_SUPER';
-- 为管理员角色添加菜单权限
INSERT INTO `RoleMenus` (`RoleId`, `MenuId`)
SELECT r.Id, 22 FROM `Roles` r WHERE r.RoleCode = 'R_ADMIN';