Compare commits

...

3 Commits

Author SHA1 Message Date
bd64e889fd fix: 修复远程桌面键盘输入问题 - 添加iframe自动聚焦功能 2026-01-21 16:36:58 +08:00
915a5ac60b feat: 完善角色管理增删改查功能 2026-01-21 16:27:59 +08:00
eebbacafde feat: 实现OS设备扫描和UUID绑定功能
- 添加OsDevice模型和OsDevicesController
- 实现WindowsScannerService用于网络扫描和WMI查询
- 添加AMT设备UUID查询功能(从CIM_ComputerSystemPackage获取PlatformGUID)
- 实现PlatformGUID到标准UUID格式的转换(字节序转换)
- 修复HardwareInfoRepository保存UUID的问题
- 前端添加OS设备管理页面和UUID获取/刷新按钮
- 添加数据库迁移脚本
2026-01-21 16:16:48 +08:00
29 changed files with 3752 additions and 67 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

@ -58,6 +58,65 @@ export function fetchGetRoleList(params: Api.SystemManage.RoleSearchParams) {
})
}
// 获取所有角色(下拉选择用)
export function fetchGetAllRoles() {
return request.get<{ roleId: number; roleName: string; roleCode: string }[]>({
url: '/api/role/all'
})
}
// 创建角色
export function fetchCreateRole(data: {
roleName: string
roleCode: string
description?: string
enabled?: boolean
}) {
return request.post({
url: '/api/role',
params: data,
showSuccessMessage: true
})
}
// 更新角色
export function fetchUpdateRole(id: number, data: {
roleName?: string
roleCode?: string
description?: string
enabled?: boolean
}) {
return request.put({
url: `/api/role/${id}`,
params: data,
showSuccessMessage: true
})
}
// 删除角色
export function fetchDeleteRole(id: number) {
return request.del({
url: `/api/role/${id}`,
showSuccessMessage: true
})
}
// 获取角色的菜单权限
export function fetchGetRoleMenus(roleId: number) {
return request.get<number[]>({
url: `/api/role/${roleId}/menus`
})
}
// 设置角色的菜单权限
export function fetchSetRoleMenus(roleId: number, menuIds: number[]) {
return request.put({
url: `/api/role/${roleId}/menus`,
params: { menuIds },
showSuccessMessage: true
})
}
// 获取菜单列表
export function fetchGetMenuList() {
return request.get<AppRouteRecord[]>({

View File

@ -28,6 +28,23 @@
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
<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">
<template #default="{ row }">
{{ row.majorVersion }}.{{ row.minorVersion }}
@ -130,7 +147,7 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
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 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 actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },

View File

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

View File

@ -95,8 +95,19 @@
</div>
<!-- 远程桌面 iframe -->
<div v-else class="remote-desktop-container">
<iframe ref="rdpFrame" :src="connectionUrl" class="rdp-iframe" allowfullscreen />
<div v-else class="remote-desktop-container" @click="focusIframe">
<div class="focus-hint" v-if="showFocusHint">
<span>点击此处激活键盘输入</span>
</div>
<iframe
ref="rdpFrame"
:src="connectionUrl"
class="rdp-iframe"
allowfullscreen
@load="onIframeLoad"
@mouseenter="focusIframe"
tabindex="0"
/>
</div>
</ElDialog>
</template>
@ -131,6 +142,22 @@ const tokenForm = ref({ expiresInMinutes: 30, maxUseCount: 1, note: '' })
const generatedToken = ref<any>(null)
const deviceTokens = ref<any[]>([])
const loadingTokens = ref(false)
const showFocusHint = ref(true)
// iframe
const focusIframe = () => {
if (rdpFrame.value) {
rdpFrame.value.focus()
showFocusHint.value = false
}
}
// iframe
const onIframeLoad = () => {
setTimeout(() => {
focusIframe()
}, 500)
}
const loadDeviceTokens = async () => {
if (!props.device?.id) return
@ -227,6 +254,7 @@ watch(() => props.modelValue, (newVal) => {
connectionUrl.value = ''
generatedToken.value = null
activeTab.value = 'quick'
showFocusHint.value = true
loadDeviceTokens()
}
})
@ -241,8 +269,10 @@ watch(() => props.modelValue, (newVal) => {
.generated-link { margin-top: 20px; }
.link-box { margin-top: 10px; }
.tokens-list { padding: 10px; }
.remote-desktop-container { width: 100%; height: calc(85vh - 100px); min-height: 600px; display: flex; justify-content: center; align-items: center; background: #1a1a1a; }
.rdp-iframe { width: 100%; height: 100%; border: none; background: transparent; }
.remote-desktop-container { width: 100%; height: calc(85vh - 100px); min-height: 600px; display: flex; flex-direction: column; justify-content: center; align-items: center; background: #1a1a1a; position: relative; }
.focus-hint { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); color: #fff; padding: 8px 16px; border-radius: 4px; font-size: 14px; z-index: 10; animation: fadeOut 3s forwards; }
@keyframes fadeOut { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; pointer-events: none; } }
.rdp-iframe { width: 100%; height: 100%; border: none; background: transparent; outline: none; }
:deep(.el-dialog__body) { padding: 0; overflow: hidden; }
:deep(.el-dialog__header) { padding: 15px 20px; margin: 0; border-bottom: 1px solid #e4e7ed; }
</style>

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

@ -39,7 +39,7 @@
</div>
<!-- 远程桌面 iframe -->
<div v-else class="remote-desktop-container">
<div v-else class="remote-desktop-container" @click="focusIframe">
<div class="toolbar">
<span>远程桌面 - {{ tokenInfo?.deviceIp }}</span>
<div class="toolbar-actions">
@ -49,7 +49,18 @@
</el-button>
</div>
</div>
<iframe ref="rdpFrame" :src="connectionUrl" class="rdp-iframe" allowfullscreen />
<div class="focus-hint" v-if="showFocusHint">
<span>点击此处激活键盘输入</span>
</div>
<iframe
ref="rdpFrame"
:src="connectionUrl"
class="rdp-iframe"
allowfullscreen
@load="onIframeLoad"
@mouseenter="focusIframe"
tabindex="0"
/>
</div>
</div>
</template>
@ -73,6 +84,22 @@ const connectionUrl = ref('')
const connecting = ref(false)
const isFullscreen = ref(false)
const rdpFrame = ref<HTMLIFrameElement | null>(null)
const showFocusHint = ref(true)
// iframe
const focusIframe = () => {
if (rdpFrame.value) {
rdpFrame.value.focus()
showFocusHint.value = false
}
}
// iframe
const onIframeLoad = () => {
setTimeout(() => {
focusIframe()
}, 500)
}
onMounted(async () => {
const token = route.params.token as string
@ -183,6 +210,7 @@ const formatDate = (dateStr: string) => {
display: flex;
flex-direction: column;
background: #1a1a1a;
position: relative;
}
.toolbar {
@ -204,5 +232,26 @@ const formatDate = (dateStr: string) => {
width: 100%;
border: none;
background: transparent;
outline: none;
}
.focus-hint {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
z-index: 10;
animation: fadeOut 3s forwards;
}
@keyframes fadeOut {
0% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; pointer-events: none; }
}
</style>

View File

@ -58,12 +58,12 @@
<script setup lang="ts">
import { ButtonMoreItem } from '@/components/core/forms/art-button-more/index.vue'
import { useTable } from '@/hooks/core/useTable'
import { fetchGetRoleList } from '@/api/system-manage'
import { fetchGetRoleList, fetchDeleteRole } from '@/api/system-manage'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import RoleSearch from './modules/role-search.vue'
import RoleEditDialog from './modules/role-edit-dialog.vue'
import RolePermissionDialog from './modules/role-permission-dialog.vue'
import { ElTag, ElMessageBox } from 'element-plus'
import { ElTag, ElMessageBox, ElMessage } from 'element-plus'
defineOptions({ name: 'Role' })
@ -224,19 +224,20 @@
currentRoleData.value = row
}
const deleteRole = (row: RoleListItem) => {
ElMessageBox.confirm(`确定删除角色"${row.roleName}"吗?此操作不可恢复!`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// TODO:
ElMessage.success('删除成功')
refreshData()
})
.catch(() => {
ElMessage.info('已取消删除')
const deleteRole = async (row: RoleListItem) => {
try {
await ElMessageBox.confirm(`确定删除角色"${row.roleName}"吗?此操作不可恢复!`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await fetchDeleteRole(row.roleId)
refreshData()
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除角色失败:', error)
}
}
}
</script>

View File

@ -27,13 +27,14 @@
</ElForm>
<template #footer>
<ElButton @click="handleClose">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
<ElButton type="primary" :loading="submitting" @click="handleSubmit">提交</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { fetchCreateRole, fetchUpdateRole } from '@/api/system-manage'
type RoleListItem = Api.SystemManage.RoleListItem
@ -57,6 +58,7 @@
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const submitting = ref(false)
/**
* 弹窗显示状态双向绑定
@ -77,8 +79,7 @@
roleCode: [
{ required: true, message: '请输入角色编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
description: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
]
})
/**
@ -150,13 +151,30 @@
try {
await formRef.value.validate()
// TODO: /
const message = props.dialogType === 'add' ? '新增成功' : '修改成功'
ElMessage.success(message)
submitting.value = true
if (props.dialogType === 'add') {
await fetchCreateRole({
roleName: form.roleName,
roleCode: form.roleCode,
description: form.description || undefined,
enabled: form.enabled
})
} else {
await fetchUpdateRole(form.roleId, {
roleName: form.roleName,
roleCode: form.roleCode,
description: form.description || undefined,
enabled: form.enabled
})
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
} finally {
submitting.value = false
}
}
</script>

View File

@ -12,9 +12,9 @@
ref="treeRef"
:data="processedMenuList"
show-checkbox
node-key="name"
node-key="id"
:default-expand-all="isExpandAll"
:default-checked-keys="[1, 2, 3]"
:default-checked-keys="checkedMenuIds"
:props="defaultProps"
@check="handleTreeCheck"
>
@ -29,13 +29,11 @@
</ElTree>
</ElScrollbar>
<template #footer>
<ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton>
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
isSelectAll ? '取消全选' : '全部选择'
}}</ElButton>
<ElButton type="primary" @click="savePermission">保存</ElButton>
<ElButton type="primary" :loading="saving" @click="savePermission">保存</ElButton>
</template>
</ElDialog>
</template>
@ -43,6 +41,7 @@
<script setup lang="ts">
import { useMenuStore } from '@/store/modules/menu'
import { formatMenuTitle } from '@/utils/router'
import { fetchGetRoleMenus, fetchSetRoleMenus } from '@/api/system-manage'
type RoleListItem = Api.SystemManage.RoleListItem
@ -67,6 +66,8 @@
const treeRef = ref()
const isExpandAll = ref(true)
const isSelectAll = ref(false)
const checkedMenuIds = ref<number[]>([])
const saving = ref(false)
/**
* 弹窗显示状态双向绑定
@ -141,10 +142,19 @@
*/
watch(
() => props.modelValue,
(newVal) => {
async (newVal) => {
if (newVal && props.roleData) {
// TODO:
console.log('设置权限:', props.roleData)
try {
const menuIds = await fetchGetRoleMenus(props.roleData.roleId)
checkedMenuIds.value = menuIds || []
// DOM
nextTick(() => {
treeRef.value?.setCheckedKeys(checkedMenuIds.value)
})
} catch (error) {
console.error('获取角色菜单权限失败:', error)
checkedMenuIds.value = []
}
}
}
)
@ -154,17 +164,35 @@
*/
const handleClose = () => {
visible.value = false
checkedMenuIds.value = []
treeRef.value?.setCheckedKeys([])
}
/**
* 保存权限配置
*/
const savePermission = () => {
// TODO:
ElMessage.success('权限保存成功')
emit('success')
handleClose()
const savePermission = async () => {
if (!props.roleData) return
const tree = treeRef.value
if (!tree) return
// IDID
const checkedKeys = tree.getCheckedKeys()
const halfCheckedKeys = tree.getHalfCheckedKeys()
const allKeys = [...checkedKeys, ...halfCheckedKeys]
const menuIds = allKeys.filter((key: any) => typeof key === 'number') as number[]
saving.value = true
try {
await fetchSetRoleMenus(props.roleData.roleId, menuIds)
emit('success')
handleClose()
} catch (error) {
console.error('保存权限失败:', error)
} finally {
saving.value = false
}
}
/**
@ -205,11 +233,11 @@
* @param nodes 节点列表
* @returns 所有节点的 key 数组
*/
const getAllNodeKeys = (nodes: MenuNode[]): string[] => {
const keys: string[] = []
const getAllNodeKeys = (nodes: MenuNode[]): (string | number)[] => {
const keys: (string | number)[] = []
const traverse = (nodeList: MenuNode[]): void => {
nodeList.forEach((node) => {
if (node.name) keys.push(node.name)
if (node.id !== undefined) keys.push(node.id)
if (node.children?.length) traverse(node.children)
})
}
@ -230,25 +258,4 @@
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
/**
* 输出选中的权限数据到控制台
* 用于调试和查看当前选中的权限配置
*/
const outputSelectedData = () => {
const tree = treeRef.value
if (!tree) return
const selectedData = {
checkedKeys: tree.getCheckedKeys(),
halfCheckedKeys: tree.getHalfCheckedKeys(),
checkedNodes: tree.getCheckedNodes(),
halfCheckedNodes: tree.getHalfCheckedNodes(),
totalChecked: tree.getCheckedKeys().length,
totalHalfChecked: tree.getHalfCheckedKeys().length
}
console.log('=== 选中的权限数据 ===', selectedData)
ElMessage.success(`已输出选中数据到控制台,共选中 ${selectedData.totalChecked} 个节点`)
}
</script>

View File

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

@ -67,6 +67,231 @@ public class RoleController : ControllerBase
Total = total
}));
}
/// <summary>
/// 获取所有角色(下拉选择用)
/// </summary>
[HttpGet("all")]
public async Task<ActionResult<ApiResponse<List<RoleSimpleDto>>>> GetAllRoles()
{
var roles = await _context.Roles
.Where(r => r.Enabled)
.OrderBy(r => r.Id)
.Select(r => new RoleSimpleDto
{
RoleId = r.Id,
RoleName = r.RoleName,
RoleCode = r.RoleCode
})
.ToListAsync();
return Ok(ApiResponse<List<RoleSimpleDto>>.Success(roles));
}
/// <summary>
/// 获取角色详情
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<RoleListItemDto>>> GetRole(int id)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound(ApiResponse<RoleListItemDto>.Fail(404, "角色不存在"));
}
return Ok(ApiResponse<RoleListItemDto>.Success(new RoleListItemDto
{
RoleId = role.Id,
RoleName = role.RoleName,
RoleCode = role.RoleCode,
Description = role.Description,
Enabled = role.Enabled,
CreateTime = role.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
}));
}
/// <summary>
/// 创建角色
/// </summary>
[HttpPost]
public async Task<ActionResult<ApiResponse<RoleListItemDto>>> CreateRole([FromBody] CreateRoleDto dto)
{
// 检查角色编码是否已存在
if (await _context.Roles.AnyAsync(r => r.RoleCode == dto.RoleCode))
{
return BadRequest(ApiResponse<RoleListItemDto>.Fail(400, "角色编码已存在"));
}
// 检查角色名称是否已存在
if (await _context.Roles.AnyAsync(r => r.RoleName == dto.RoleName))
{
return BadRequest(ApiResponse<RoleListItemDto>.Fail(400, "角色名称已存在"));
}
var role = new Role
{
RoleName = dto.RoleName,
RoleCode = dto.RoleCode,
Description = dto.Description,
Enabled = dto.Enabled,
CreatedAt = DateTime.UtcNow
};
_context.Roles.Add(role);
await _context.SaveChangesAsync();
return Ok(ApiResponse<RoleListItemDto>.Success(new RoleListItemDto
{
RoleId = role.Id,
RoleName = role.RoleName,
RoleCode = role.RoleCode,
Description = role.Description,
Enabled = role.Enabled,
CreateTime = role.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
}, "创建成功"));
}
/// <summary>
/// 更新角色
/// </summary>
[HttpPut("{id}")]
public async Task<ActionResult<ApiResponse<RoleListItemDto>>> UpdateRole(int id, [FromBody] UpdateRoleDto dto)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound(ApiResponse<RoleListItemDto>.Fail(404, "角色不存在"));
}
// 检查角色编码是否与其他角色重复
if (!string.IsNullOrEmpty(dto.RoleCode) && dto.RoleCode != role.RoleCode)
{
if (await _context.Roles.AnyAsync(r => r.RoleCode == dto.RoleCode && r.Id != id))
{
return BadRequest(ApiResponse<RoleListItemDto>.Fail(400, "角色编码已存在"));
}
role.RoleCode = dto.RoleCode;
}
// 检查角色名称是否与其他角色重复
if (!string.IsNullOrEmpty(dto.RoleName) && dto.RoleName != role.RoleName)
{
if (await _context.Roles.AnyAsync(r => r.RoleName == dto.RoleName && r.Id != id))
{
return BadRequest(ApiResponse<RoleListItemDto>.Fail(400, "角色名称已存在"));
}
role.RoleName = dto.RoleName;
}
if (dto.Description != null)
{
role.Description = dto.Description;
}
if (dto.Enabled.HasValue)
{
role.Enabled = dto.Enabled.Value;
}
await _context.SaveChangesAsync();
return Ok(ApiResponse<RoleListItemDto>.Success(new RoleListItemDto
{
RoleId = role.Id,
RoleName = role.RoleName,
RoleCode = role.RoleCode,
Description = role.Description,
Enabled = role.Enabled,
CreateTime = role.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
}, "更新成功"));
}
/// <summary>
/// 删除角色
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult<ApiResponse<object>>> DeleteRole(int id)
{
var role = await _context.Roles
.Include(r => r.UserRoles)
.Include(r => r.RoleMenus)
.FirstOrDefaultAsync(r => r.Id == id);
if (role == null)
{
return NotFound(ApiResponse<object>.Fail(404, "角色不存在"));
}
// 检查是否有用户使用该角色
if (role.UserRoles.Any())
{
return BadRequest(ApiResponse<object>.Fail(400, "该角色下存在用户,无法删除"));
}
// 删除角色菜单关联
_context.RoleMenus.RemoveRange(role.RoleMenus);
// 删除角色
_context.Roles.Remove(role);
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "删除成功"));
}
/// <summary>
/// 获取角色的菜单权限
/// </summary>
[HttpGet("{id}/menus")]
public async Task<ActionResult<ApiResponse<List<int>>>> GetRoleMenus(int id)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound(ApiResponse<List<int>>.Fail(404, "角色不存在"));
}
var menuIds = await _context.RoleMenus
.Where(rm => rm.RoleId == id)
.Select(rm => rm.MenuId)
.ToListAsync();
return Ok(ApiResponse<List<int>>.Success(menuIds));
}
/// <summary>
/// 设置角色的菜单权限
/// </summary>
[HttpPut("{id}/menus")]
public async Task<ActionResult<ApiResponse<object>>> SetRoleMenus(int id, [FromBody] SetRoleMenusDto dto)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound(ApiResponse<object>.Fail(404, "角色不存在"));
}
// 删除原有的菜单权限
var existingMenus = await _context.RoleMenus
.Where(rm => rm.RoleId == id)
.ToListAsync();
_context.RoleMenus.RemoveRange(existingMenus);
// 添加新的菜单权限
if (dto.MenuIds != null && dto.MenuIds.Any())
{
var newMenus = dto.MenuIds.Select(menuId => new RoleMenu
{
RoleId = id,
MenuId = menuId
});
_context.RoleMenus.AddRange(newMenus);
}
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "菜单权限设置成功"));
}
}
/// <summary>
@ -81,3 +306,43 @@ public class RoleListItemDto
public bool Enabled { get; set; }
public string CreateTime { get; set; } = string.Empty;
}
/// <summary>
/// 角色简单 DTO下拉选择用
/// </summary>
public class RoleSimpleDto
{
public int RoleId { get; set; }
public string RoleName { get; set; } = string.Empty;
public string RoleCode { get; set; } = string.Empty;
}
/// <summary>
/// 创建角色 DTO
/// </summary>
public class CreateRoleDto
{
public string RoleName { get; set; } = string.Empty;
public string RoleCode { get; set; } = string.Empty;
public string? Description { get; set; }
public bool Enabled { get; set; } = true;
}
/// <summary>
/// 更新角色 DTO
/// </summary>
public class UpdateRoleDto
{
public string? RoleName { get; set; }
public string? RoleCode { get; set; }
public string? Description { get; set; }
public bool? Enabled { get; set; }
}
/// <summary>
/// 设置角色菜单 DTO
/// </summary>
public class SetRoleMenusDto
{
public List<int>? MenuIds { get; set; }
}

View File

@ -17,6 +17,7 @@ public class AppDbContext : DbContext
public DbSet<StorageDevice> StorageDevices { get; set; }
public DbSet<WindowsCredential> WindowsCredentials { get; set; }
public DbSet<RemoteAccessToken> RemoteAccessTokens { get; set; }
public DbSet<OsDevice> OsDevices { get; set; }
// 用户认证相关
public DbSet<User> Users { get; set; }
@ -161,5 +162,35 @@ public class AppDbContext : DbContext
.WithMany(m => m.RoleMenus)
.HasForeignKey(rm => rm.MenuId)
.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")
.HasColumnType("int");
b.Property<string>("SystemUuid")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("WindowsPassword")
.HasColumnType("longtext");
@ -106,6 +110,8 @@ namespace AmtScanner.Api.Migrations
b.HasIndex("IpAddress")
.IsUnique();
b.HasIndex("SystemUuid");
b.ToTable("AmtDevices");
});
@ -148,6 +154,9 @@ namespace AmtScanner.Api.Migrations
b.Property<string>("SystemSerialNumber")
.HasColumnType("longtext");
b.Property<string>("SystemUuid")
.HasColumnType("longtext");
b.Property<long?>("TotalMemoryBytes")
.HasColumnType("bigint");
@ -273,6 +282,72 @@ namespace AmtScanner.Api.Migrations
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")
@ -562,6 +637,16 @@ namespace AmtScanner.Api.Migrations
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")

View File

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

View File

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

View File

@ -16,6 +16,7 @@ public class SystemInfoDto
public string? Manufacturer { get; set; }
public string? Model { get; set; }
public string? SerialNumber { get; set; }
public string? Uuid { get; set; }
}
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<IHardwareInfoRepository, HardwareInfoRepository>();
builder.Services.AddScoped<IAmtPowerService, AmtPowerService>();
builder.Services.AddScoped<IWindowsScannerService, WindowsScannerService>();
builder.Services.AddHttpClient<IGuacamoleService, GuacamoleService>();
// Add JWT Configuration

View File

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

View File

@ -152,6 +152,9 @@ public class AmtHardwareQueryService : IAmtHardwareQueryService
_logger.LogDebug("System info: {Manufacturer} {Model}",
hardwareInfo.SystemManufacturer, hardwareInfo.SystemModel);
// Query UUID from CIM_ComputerSystemPackage (在同一个 try 块内查询,确保连接有效)
QuerySystemUuid(connection, hardwareInfo);
}
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)
{
try

View File

@ -89,6 +89,15 @@ public class HardwareInfoService : IHardwareInfoService
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
await _repository.SaveAsync(hardwareInfo);
@ -149,7 +158,8 @@ public class HardwareInfoService : IHardwareInfoService
{
Manufacturer = hardwareInfo.SystemManufacturer,
Model = hardwareInfo.SystemModel,
SerialNumber = hardwareInfo.SystemSerialNumber
SerialNumber = hardwareInfo.SystemSerialNumber,
Uuid = hardwareInfo.SystemUuid
},
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';