feat: 修复远程桌面API路由、添加桌面管理菜单、设备Windows凭据功能

This commit is contained in:
lvfengfree 2026-01-20 21:41:33 +08:00
parent 5382685f21
commit c546d4635a
20 changed files with 1628 additions and 247 deletions

View File

@ -42,6 +42,24 @@ export const deviceApi = {
}) })
}, },
// 手动添加设备
addDevice(data: { ipAddress: string; hostname?: string; description?: string; windowsUsername?: string; windowsPassword?: string }) {
return request.post({
url: '/api/devices',
params: data,
showSuccessMessage: true
})
},
// 更新设备
updateDevice(id: number, data: { hostname?: string; description?: string }) {
return request.put({
url: `/api/devices/${id}`,
params: data,
showSuccessMessage: true
})
},
// 删除设备 // 删除设备
deleteDevice(id: number) { deleteDevice(id: number) {
return request.del({ return request.del({
@ -62,6 +80,22 @@ export const deviceApi = {
return request.get({ return request.get({
url: `/api/devices/${id}/status` url: `/api/devices/${id}/status`
}) })
},
// 设置设备 Windows 凭据
setDeviceCredentials(id: number, data: { username?: string; password?: string }) {
return request.put({
url: `/api/devices/${id}/credentials`,
params: data,
showSuccessMessage: true
})
},
// 获取设备 Windows 凭据
getDeviceCredentials(id: number) {
return request.get({
url: `/api/devices/${id}/credentials`
})
} }
} }
@ -221,7 +255,7 @@ export const remoteDesktopApi = {
// 直接连接(需要凭据) // 直接连接(需要凭据)
connect(deviceId: number, credentials: { username: string; password: string; domain?: string }) { connect(deviceId: number, credentials: { username: string; password: string; domain?: string }) {
return request.post({ return request.post({
url: `/api/remotedesktop/connect/${deviceId}`, url: `/api/remote-desktop/connect/${deviceId}`,
params: credentials params: credentials
}) })
}, },
@ -229,7 +263,7 @@ export const remoteDesktopApi = {
// 生成访问 Token // 生成访问 Token
generateToken(deviceId: number, options: { credentialId?: number; expiresInMinutes?: number; maxUseCount?: number; note?: string } = {}) { generateToken(deviceId: number, options: { credentialId?: number; expiresInMinutes?: number; maxUseCount?: number; note?: string } = {}) {
return request.post({ return request.post({
url: `/api/remotedesktop/generate-token/${deviceId}`, url: `/api/remote-desktop/generate-token/${deviceId}`,
params: options params: options
}) })
}, },
@ -237,28 +271,28 @@ export const remoteDesktopApi = {
// 通过 Token 连接 // 通过 Token 连接
connectByToken(token: string) { connectByToken(token: string) {
return request.get({ return request.get({
url: `/api/remotedesktop/connect-by-token/${token}` url: `/api/remote-desktop/connect-by-token/${token}`
}) })
}, },
// 验证 Token // 验证 Token
validateToken(token: string) { validateToken(token: string) {
return request.get({ return request.get({
url: `/api/remotedesktop/validate-token/${token}` url: `/api/remote-desktop/validate-token/${token}`
}) })
}, },
// 获取设备的所有 Token // 获取设备的所有 Token
getDeviceTokens(deviceId: number) { getDeviceTokens(deviceId: number) {
return request.get<any[]>({ return request.get<any[]>({
url: `/api/remotedesktop/list-tokens/${deviceId}` url: `/api/remote-desktop/list-tokens/${deviceId}`
}) })
}, },
// 撤销 Token // 撤销 Token
revokeToken(tokenId: number) { revokeToken(tokenId: number) {
return request.del({ return request.del({
url: `/api/remotedesktop/revoke-token/${tokenId}`, url: `/api/remote-desktop/revoke-token/${tokenId}`,
showSuccessMessage: true showSuccessMessage: true
}) })
}, },
@ -266,14 +300,14 @@ export const remoteDesktopApi = {
// 清理过期 Token // 清理过期 Token
cleanupTokens() { cleanupTokens() {
return request.post({ return request.post({
url: '/api/remotedesktop/cleanup-tokens' url: '/api/remote-desktop/cleanup-tokens'
}) })
}, },
// 测试 Guacamole 连接 // 测试 Guacamole 连接
test() { test() {
return request.get({ return request.get({
url: '/api/remotedesktop/test' url: '/api/remote-desktop/test'
}) })
} }
} }

View File

@ -3,7 +3,7 @@
<ElCard shadow="never"> <ElCard shadow="never">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>AMT 设备列表</span> <span>AMT 设备管理</span>
<div class="header-actions"> <div class="header-actions">
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px"> <ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
<el-icon class="is-loading"><Refresh /></el-icon> <el-icon class="is-loading"><Refresh /></el-icon>
@ -54,17 +54,26 @@
</ElTag> </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="Windows账号" width="120">
<template #default="{ row }">
<ElTag v-if="row.windowsUsername" type="success" size="small">{{ row.windowsUsername }}</ElTag>
<ElTag v-else type="warning" size="small">未配置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="hostname" label="主机名" min-width="120" /> <ElTableColumn prop="hostname" label="主机名" min-width="120" />
<ElTableColumn label="发现时间" width="160"> <ElTableColumn label="发现时间" width="160">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDateTime(row.discoveredAt) }} {{ formatDateTime(row.discoveredAt) }}
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="操作" width="380" fixed="right"> <ElTableColumn label="操作" width="420" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline"> <ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline || !row.windowsUsername">
远程桌面 远程桌面
</ElButton> </ElButton>
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
配置账号
</ElButton>
<ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px"> <ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
<ElButton type="warning" size="small"> <ElButton type="warning" size="small">
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon> 电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
@ -95,6 +104,25 @@
<!-- 远程桌面弹窗 --> <!-- 远程桌面弹窗 -->
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" /> <RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
<!-- 配置 Windows 账号弹窗 -->
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
<ElForm :model="credentialsForm" label-width="100px">
<ElFormItem label="设备 IP">
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
</ElFormItem>
<ElFormItem label="用户名">
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="credentialsForm.password" type="password" placeholder="Windows 登录密码" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showCredentialsDialog = false">取消</ElButton>
<ElButton type="primary" @click="saveCredentials" :loading="savingCredentials">保存</ElButton>
</template>
</ElDialog>
</div> </div>
</template> </template>
@ -114,8 +142,11 @@ const isCheckingStatus = ref(false)
const searchKeyword = ref('') const searchKeyword = ref('')
const showHardwareModal = ref(false) const showHardwareModal = ref(false)
const showRemoteDesktopModal = ref(false) const showRemoteDesktopModal = ref(false)
const showCredentialsDialog = ref(false)
const selectedDeviceId = ref(0) const selectedDeviceId = ref(0)
const selectedDevice = ref<any>(null) const selectedDevice = ref<any>(null)
const credentialsForm = ref({ username: '', password: '' })
const savingCredentials = ref(false)
let statusCheckInterval: number | null = null let statusCheckInterval: number | null = null
@ -223,10 +254,43 @@ const handleRemoteDesktop = (device: any) => {
ElMessage.warning('设备操作系统未运行,无法连接远程桌面') ElMessage.warning('设备操作系统未运行,无法连接远程桌面')
return return
} }
if (!device.windowsUsername) {
ElMessage.warning('请先配置该设备的 Windows 登录账号')
return
}
selectedDevice.value = device selectedDevice.value = device
showRemoteDesktopModal.value = true showRemoteDesktopModal.value = true
} }
const handleSetCredentials = async (device: any) => {
selectedDevice.value = device
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
showCredentialsDialog.value = true
}
const saveCredentials = async () => {
if (!credentialsForm.value.username) {
ElMessage.warning('请输入用户名')
return
}
savingCredentials.value = true
try {
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
//
const device = devices.value.find(d => d.id === selectedDevice.value.id)
if (device) {
device.windowsUsername = credentialsForm.value.username
}
showCredentialsDialog.value = false
ElMessage.success('账号配置成功')
} catch (error) {
ElMessage.error('保存失败')
} finally {
savingCredentials.value = false
}
}
const handlePowerCommand = async (command: string, device: any) => { const handlePowerCommand = async (command: string, device: any) => {
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = { const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' }, 'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },

View File

@ -29,8 +29,11 @@
<ElTabPane label="快速连接" name="quick"> <ElTabPane label="快速连接" name="quick">
<div class="quick-connect"> <div class="quick-connect">
<ElAlert type="info" :closable="false" style="margin-bottom: 20px"> <ElAlert type="info" :closable="false" style="margin-bottom: 20px">
使用默认 Windows 凭据快速连接无需输入密码 使用设备配置的 Windows 账号快速连接
</ElAlert> </ElAlert>
<div v-if="device?.windowsUsername" style="margin-bottom: 20px">
<ElTag type="success">当前账号: {{ device.windowsUsername }}</ElTag>
</div>
<ElButton type="primary" size="large" @click="quickConnect" :loading="connecting"> <ElButton type="primary" size="large" @click="quickConnect" :loading="connecting">
一键连接 一键连接
</ElButton> </ElButton>
@ -40,13 +43,6 @@
<ElTabPane label="生成分享链接" name="share"> <ElTabPane label="生成分享链接" name="share">
<div class="share-form"> <div class="share-form">
<ElForm :model="tokenForm" label-width="120px"> <ElForm :model="tokenForm" label-width="120px">
<ElFormItem label="选择凭据">
<ElSelect v-model="tokenForm.credentialId" placeholder="使用默认凭据" clearable style="width: 100%">
<ElOption v-for="cred in credentials" :key="cred.id"
:label="`${cred.name} (${cred.username})${cred.isDefault ? ' [默认]' : ''}`"
:value="cred.id" />
</ElSelect>
</ElFormItem>
<ElFormItem label="有效期(分钟)"> <ElFormItem label="有效期(分钟)">
<ElInputNumber v-model="tokenForm.expiresInMinutes" :min="5" :max="1440" /> <ElInputNumber v-model="tokenForm.expiresInMinutes" :min="5" :max="1440" />
</ElFormItem> </ElFormItem>
@ -73,22 +69,6 @@
</div> </div>
</ElTabPane> </ElTabPane>
<ElTabPane label="手动输入" name="manual">
<div class="credentials-form">
<ElForm :model="manualCredentials" label-width="100px">
<ElFormItem label="用户名">
<ElInput v-model="manualCredentials.username" placeholder="Windows 用户名" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="manualCredentials.password" type="password" placeholder="Windows 密码" show-password />
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="manualConnect" :loading="connecting">连接</ElButton>
</ElFormItem>
</ElForm>
</div>
</ElTabPane>
<ElTabPane label="链接管理" name="tokens"> <ElTabPane label="链接管理" name="tokens">
<div class="tokens-list"> <div class="tokens-list">
<ElTable :data="deviceTokens" v-loading="loadingTokens" size="small"> <ElTable :data="deviceTokens" v-loading="loadingTokens" size="small">
@ -124,7 +104,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { windowsCredentialsApi, remoteDesktopApi } from '@/api/amt' import { remoteDesktopApi } from '@/api/amt'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
@ -147,21 +127,11 @@ const connecting = ref(false)
const generating = ref(false) const generating = ref(false)
const isFullscreen = ref(false) const isFullscreen = ref(false)
const rdpFrame = ref<HTMLIFrameElement | null>(null) const rdpFrame = ref<HTMLIFrameElement | null>(null)
const credentials = ref<any[]>([]) const tokenForm = ref({ expiresInMinutes: 30, maxUseCount: 1, note: '' })
const manualCredentials = ref({ username: '', password: '' })
const tokenForm = ref({ credentialId: null as number | null, expiresInMinutes: 30, maxUseCount: 1, note: '' })
const generatedToken = ref<any>(null) const generatedToken = ref<any>(null)
const deviceTokens = ref<any[]>([]) const deviceTokens = ref<any[]>([])
const loadingTokens = ref(false) const loadingTokens = ref(false)
const loadCredentials = async () => {
try {
credentials.value = await windowsCredentialsApi.getAll()
} catch (error) {
console.error('加载凭据失败', error)
}
}
const loadDeviceTokens = async () => { const loadDeviceTokens = async () => {
if (!props.device?.id) return if (!props.device?.id) return
loadingTokens.value = true loadingTokens.value = true
@ -190,28 +160,7 @@ const quickConnect = async () => {
ElMessage.error(connectResponse.error || '连接失败') ElMessage.error(connectResponse.error || '连接失败')
} }
} catch (error: any) { } catch (error: any) {
ElMessage.error(error.message || '连接失败,请先配置 Windows 凭据') ElMessage.error(error.message || '连接失败,请先配置设备的 Windows 登录账号')
} finally {
connecting.value = false
}
}
const manualConnect = async () => {
if (!manualCredentials.value.username) {
ElMessage.warning('请输入用户名')
return
}
connecting.value = true
try {
const response = await remoteDesktopApi.connect(props.device.id, manualCredentials.value)
if (response.success) {
connectionUrl.value = response.connectionUrl
ElMessage.success('正在连接远程桌面...')
} else {
ElMessage.error(response.error || '连接失败')
}
} catch (error: any) {
ElMessage.error(error.message || '连接远程桌面服务失败')
} finally { } finally {
connecting.value = false connecting.value = false
} }
@ -221,13 +170,11 @@ const generateToken = async () => {
generating.value = true generating.value = true
try { try {
const response = await remoteDesktopApi.generateToken(props.device.id, { const response = await remoteDesktopApi.generateToken(props.device.id, {
credentialId: tokenForm.value.credentialId || undefined,
expiresInMinutes: tokenForm.value.expiresInMinutes, expiresInMinutes: tokenForm.value.expiresInMinutes,
maxUseCount: tokenForm.value.maxUseCount, maxUseCount: tokenForm.value.maxUseCount,
note: tokenForm.value.note || undefined note: tokenForm.value.note || undefined
}) })
if (response.success) { if (response.success) {
// accessUrl Hash
response.accessUrl = `${window.location.origin}/#/remote/${response.token}` response.accessUrl = `${window.location.origin}/#/remote/${response.token}`
generatedToken.value = response generatedToken.value = response
ElMessage.success('链接已生成') ElMessage.success('链接已生成')
@ -250,7 +197,6 @@ const copyLink = async () => {
} }
const copyTokenLink = async (token: any) => { const copyTokenLink = async (token: any) => {
// 使 Hash
const url = `${window.location.origin}/#/remote/${token.token}` const url = `${window.location.origin}/#/remote/${token.token}`
await navigator.clipboard.writeText(url) await navigator.clipboard.writeText(url)
ElMessage.success('链接已复制') ElMessage.success('链接已复制')
@ -279,10 +225,8 @@ const handleClose = () => { connectionUrl.value = ''; visible.value = false }
watch(() => props.modelValue, (newVal) => { watch(() => props.modelValue, (newVal) => {
if (newVal) { if (newVal) {
connectionUrl.value = '' connectionUrl.value = ''
manualCredentials.value = { username: '', password: '' }
generatedToken.value = null generatedToken.value = null
activeTab.value = 'quick' activeTab.value = 'quick'
loadCredentials()
loadDeviceTokens() loadDeviceTokens()
} }
}) })
@ -293,7 +237,6 @@ watch(() => props.modelValue, (newVal) => {
.header-actions { margin-right: 40px; display: flex; align-items: center; } .header-actions { margin-right: 40px; display: flex; align-items: center; }
.connection-options { padding: 20px; } .connection-options { padding: 20px; }
.quick-connect { text-align: center; padding: 40px 20px; } .quick-connect { text-align: center; padding: 40px 20px; }
.credentials-form { max-width: 400px; margin: 0 auto; padding: 20px; }
.share-form { max-width: 500px; margin: 0 auto; padding: 20px; } .share-form { max-width: 500px; margin: 0 auto; padding: 20px; }
.generated-link { margin-top: 20px; } .generated-link { margin-top: 20px; }
.link-box { margin-top: 10px; } .link-box { margin-top: 10px; }

View File

@ -1,23 +1,26 @@
<template> <template>
<div class="scan-page"> <div class="scan-page">
<!-- 扫描配置 --> <!-- 添加方式选择 -->
<ElCard class="scan-config" shadow="never"> <ElCard shadow="never">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>网络扫描配置</span> <span>设备添加</span>
</div> </div>
</template> </template>
<ElForm :model="form" :rules="rules" ref="formRef" label-width="120px"> <ElTabs v-model="activeTab">
<!-- 网络扫描 -->
<ElTabPane label="网络扫描" name="scan">
<ElForm :model="scanForm" :rules="scanRules" ref="scanFormRef" label-width="120px">
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="网段地址" prop="networkSegment"> <ElFormItem label="网段地址" prop="networkSegment">
<ElInput v-model="form.networkSegment" placeholder="例如: 192.168.1.0" /> <ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="子网掩码" prop="subnetMask"> <ElFormItem label="子网掩码" prop="subnetMask">
<ElInput v-model="form.subnetMask" placeholder="例如: 255.255.255.0 或 /24" /> <ElInput v-model="scanForm.subnetMask" placeholder="例如: 255.255.255.0 或 /24" />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
@ -26,7 +29,7 @@
<el-icon><Search /></el-icon> <el-icon><Search /></el-icon>
开始扫描 开始扫描
</ElButton> </ElButton>
<ElButton @click="handleReset">重置</ElButton> <ElButton @click="handleResetScan">重置</ElButton>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
@ -35,10 +38,32 @@
<ElAlert <ElAlert
title="提示" title="提示"
type="info" type="info"
description="扫描将检测指定网段内所有支持 Intel AMT 的设备,请确保网络连接正常。扫描过程中请勿关闭页面。" description="扫描将检测指定网段内所有支持 Intel AMT 的设备,请确保网络连接正常。"
:closable="false" :closable="false"
show-icon show-icon
style="margin-top: 10px"
/> />
</ElTabPane>
<!-- 手动添加 -->
<ElTabPane label="手动添加" name="manual">
<ElForm :model="manualForm" :rules="manualRules" ref="manualFormRef" label-width="120px" style="max-width: 600px">
<ElFormItem label="IP 地址" prop="ipAddress">
<ElInput v-model="manualForm.ipAddress" placeholder="设备 IP 地址" />
</ElFormItem>
<ElFormItem label="主机名">
<ElInput v-model="manualForm.hostname" placeholder="可选,设备主机名" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="manualForm.description" type="textarea" :rows="2" placeholder="可选,设备备注信息" />
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="handleAddDevice" :loading="adding">添加设备</ElButton>
<ElButton @click="handleResetManual">重置</ElButton>
</ElFormItem>
</ElForm>
</ElTabPane>
</ElTabs>
</ElCard> </ElCard>
<!-- 扫描进度 --> <!-- 扫描进度 -->
@ -83,19 +108,30 @@ import { ref, reactive, computed, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import { scanApi } from '@/api/amt' import { scanApi, deviceApi } from '@/api/amt'
defineOptions({ name: 'AmtScan' }) defineOptions({ name: 'AmtScan' })
const router = useRouter() const router = useRouter()
const formRef = ref() const activeTab = ref('scan')
const scanFormRef = ref()
const manualFormRef = ref()
const scanning = ref(false) const scanning = ref(false)
const adding = ref(false)
const form = reactive({ //
const scanForm = reactive({
networkSegment: '192.168.1.0', networkSegment: '192.168.1.0',
subnetMask: '255.255.255.0' subnetMask: '255.255.255.0'
}) })
//
const manualForm = reactive({
ipAddress: '',
hostname: '',
description: ''
})
const scanProgress = reactive({ const scanProgress = reactive({
taskId: '', taskId: '',
scannedCount: 0, scannedCount: 0,
@ -114,7 +150,7 @@ const progressStatus = computed(() => {
const validateIp = (_rule: any, value: string, callback: Function) => { const validateIp = (_rule: any, value: string, callback: Function) => {
const ipRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ const ipRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
if (!value) { if (!value) {
callback(new Error('请输入网段地址')) callback(new Error('请输入 IP 地址'))
} else if (!ipRegex.test(value)) { } else if (!ipRegex.test(value)) {
callback(new Error('请输入有效的 IP 地址')) callback(new Error('请输入有效的 IP 地址'))
} else { } else {
@ -142,17 +178,22 @@ const validateSubnetMask = (_rule: any, value: string, callback: Function) => {
} }
} }
const rules = { const scanRules = {
networkSegment: [{ validator: validateIp, trigger: 'blur' }], networkSegment: [{ validator: validateIp, trigger: 'blur' }],
subnetMask: [{ validator: validateSubnetMask, trigger: 'blur' }] subnetMask: [{ validator: validateSubnetMask, trigger: 'blur' }]
} }
const manualRules = {
ipAddress: [{ validator: validateIp, trigger: 'blur' }]
}
let pollTimer: number | null = null let pollTimer: number | null = null
//
const handleStartScan = async () => { const handleStartScan = async () => {
if (!formRef.value) return if (!scanFormRef.value) return
await formRef.value.validate(async (valid: boolean) => { await scanFormRef.value.validate(async (valid: boolean) => {
if (valid) { if (valid) {
scanning.value = true scanning.value = true
scanProgress.status = 'running' scanProgress.status = 'running'
@ -162,11 +203,9 @@ const handleStartScan = async () => {
scanProgress.progressPercentage = 0 scanProgress.progressPercentage = 0
try { try {
const result = await scanApi.startScan(form.networkSegment, form.subnetMask) const result = await scanApi.startScan(scanForm.networkSegment, scanForm.subnetMask)
scanProgress.taskId = result.taskId scanProgress.taskId = result.taskId
ElMessage.success('扫描任务已启动') ElMessage.success('扫描任务已启动')
//
startPolling() startPolling()
} catch (error) { } catch (error) {
scanning.value = false scanning.value = false
@ -220,15 +259,46 @@ const handleCancelScan = async () => {
} }
} }
const handleReset = () => { const handleResetScan = () => {
formRef.value?.resetFields() scanFormRef.value?.resetFields()
}
//
const handleAddDevice = async () => {
if (!manualFormRef.value) return
await manualFormRef.value.validate(async (valid: boolean) => {
if (valid) {
adding.value = true
try {
await deviceApi.addDevice({
ipAddress: manualForm.ipAddress,
hostname: manualForm.hostname || undefined,
description: manualForm.description || undefined
})
ElMessage.success('设备添加成功')
handleResetManual()
goToDeviceList()
} catch (error: any) {
ElMessage.error(error.message || '添加设备失败')
} finally {
adding.value = false
}
}
})
}
const handleResetManual = () => {
manualFormRef.value?.resetFields()
manualForm.ipAddress = ''
manualForm.hostname = ''
manualForm.description = ''
} }
const goToDeviceList = () => { const goToDeviceList = () => {
router.push('/amt/devices') router.push('/amt/devices')
} }
//
onUnmounted(() => { onUnmounted(() => {
stopPolling() stopPolling()
}) })
@ -239,10 +309,6 @@ onUnmounted(() => {
padding: 0; padding: 0;
} }
.scan-config {
margin-bottom: 20px;
}
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -252,7 +318,7 @@ onUnmounted(() => {
} }
.progress-card { .progress-card {
margin-bottom: 20px; margin-top: 20px;
} }
.progress-info { .progress-info {

View File

@ -0,0 +1,256 @@
<template>
<div class="devices-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>远程桌面</span>
<div class="header-actions">
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
<el-icon class="is-loading"><Refresh /></el-icon>
检测中...
</ElTag>
<ElInput
v-model="searchKeyword"
placeholder="搜索 IP 地址"
style="width: 200px; margin-right: 10px"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</ElInput>
<ElButton type="primary" :icon="Refresh" @click="handleRefresh">刷新</ElButton>
</div>
</div>
</template>
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn prop="hostname" label="主机名" width="150" />
<ElTableColumn label="AMT状态" width="90">
<template #default="{ row }">
<ElTag :type="row.amtOnline ? 'success' : 'info'" size="small">
{{ row.amtOnline ? '在线' : '离线' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="系统状态" width="90">
<template #default="{ row }">
<ElTag :type="row.osOnline ? 'success' : 'danger'" size="small">
{{ row.osOnline ? '运行中' : '已关机' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="Windows账号" width="120">
<template #default="{ row }">
<ElTag v-if="row.windowsUsername" type="success" size="small">{{ row.windowsUsername }}</ElTag>
<ElTag v-else type="warning" size="small">未配置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="description" label="备注" min-width="150" />
<ElTableColumn label="操作" width="380" fixed="right">
<template #default="{ row }">
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline || !row.windowsUsername">
远程桌面
</ElButton>
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
配置账号
</ElButton>
<ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
<ElButton type="warning" size="small">
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="power-on" :icon="VideoPlay">开机</ElDropdownItem>
<ElDropdownItem command="power-off" :icon="VideoPause">关机</ElDropdownItem>
<ElDropdownItem command="restart" :icon="RefreshRight">重启</ElDropdownItem>
<ElDropdownItem divided command="force-off" :icon="CircleClose">强制关机</ElDropdownItem>
<ElDropdownItem command="force-restart" :icon="Refresh">强制重启</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 远程桌面弹窗 -->
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
<!-- 配置 Windows 账号弹窗 -->
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
<ElForm :model="credentialsForm" label-width="100px">
<ElFormItem label="设备 IP">
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
</ElFormItem>
<ElFormItem label="用户名">
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="credentialsForm.password" type="password" placeholder="Windows 登录密码" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showCredentialsDialog = false">取消</ElButton>
<ElButton type="primary" @click="saveCredentials" :loading="savingCredentials">保存</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
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 RemoteDesktopModal from '@/views/amt/modules/remote-desktop-modal.vue'
defineOptions({ name: 'DesktopManageDevices' })
const devices = ref<any[]>([])
const loading = ref(false)
const isCheckingStatus = ref(false)
const searchKeyword = ref('')
const showRemoteDesktopModal = ref(false)
const showCredentialsDialog = ref(false)
const selectedDevice = ref<any>(null)
const credentialsForm = ref({ username: '', password: '' })
const savingCredentials = ref(false)
let statusCheckInterval: number | null = null
onMounted(() => {
fetchDevices()
startStatusCheck()
})
onUnmounted(() => {
stopStatusCheck()
})
const fetchDevices = async () => {
loading.value = true
try {
devices.value = await deviceApi.getAllDevices()
} catch (error) {
console.error('获取设备列表失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = async () => {
if (searchKeyword.value) {
loading.value = true
try {
devices.value = await deviceApi.searchDevices(searchKeyword.value)
} catch (error) {
console.error('搜索设备失败:', error)
} finally {
loading.value = false
}
} else {
fetchDevices()
}
}
const handleRefresh = async () => {
await fetchDevices()
await checkAllDevicesStatus()
ElMessage.success('刷新成功')
}
const checkAllDevicesStatus = async () => {
if (isCheckingStatus.value || devices.value.length === 0) return
isCheckingStatus.value = true
try {
const statusList = await deviceApi.checkAllDevicesStatus()
const statusMap = new Map(statusList.map((s: any) => [s.id, { amtOnline: s.amtOnline, osOnline: s.osOnline }]))
devices.value.forEach(device => {
if (statusMap.has(device.id)) {
const status = statusMap.get(device.id)
device.amtOnline = status.amtOnline
device.osOnline = status.osOnline
}
})
} catch (error) {
console.error('检测设备状态失败:', error)
} finally {
isCheckingStatus.value = false
}
}
const startStatusCheck = () => {
checkAllDevicesStatus()
statusCheckInterval = window.setInterval(() => checkAllDevicesStatus(), 30000)
}
const stopStatusCheck = () => {
if (statusCheckInterval) { clearInterval(statusCheckInterval); statusCheckInterval = null }
}
const handleRemoteDesktop = (device: any) => {
if (!device.osOnline) { ElMessage.warning('设备操作系统未运行,无法连接远程桌面'); return }
if (!device.windowsUsername) { ElMessage.warning('请先配置该设备的 Windows 登录账号'); return }
selectedDevice.value = device
showRemoteDesktopModal.value = true
}
const handleSetCredentials = async (device: any) => {
selectedDevice.value = device
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
showCredentialsDialog.value = true
}
const saveCredentials = async () => {
if (!credentialsForm.value.username) { ElMessage.warning('请输入用户名'); return }
savingCredentials.value = true
try {
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
const device = devices.value.find(d => d.id === selectedDevice.value.id)
if (device) device.windowsUsername = credentialsForm.value.username
showCredentialsDialog.value = false
ElMessage.success('账号配置成功')
} catch (error) {
ElMessage.error('保存失败')
} finally {
savingCredentials.value = 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: '确定要开机吗?' },
'power-off': { api: powerApi.powerOff, name: '关机', confirmMsg: '确定要关机吗?' },
'restart': { api: powerApi.restart, name: '重启', confirmMsg: '确定要重启吗?' },
'force-off': { api: powerApi.forceOff, name: '强制关机', confirmMsg: '确定要强制关机吗?可能导致数据丢失!' },
'force-restart': { api: powerApi.forceRestart, name: '强制重启', confirmMsg: '确定要强制重启吗?可能导致数据丢失!' }
}
const action = actionMap[command]
if (!action) return
try {
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
confirmButtonText: '确定', cancelButtonText: '取消', type: command.includes('force') ? 'warning' : 'info'
})
ElMessage.info(`正在执行${action.name}...`)
const response = await action.api(device.id)
if (response.success) {
ElMessage.success(response.message || `${action.name}命令已发送`)
setTimeout(() => checkAllDevicesStatus(), 3000)
} else {
ElMessage.error(response.error || `${action.name}失败`)
}
} catch (error: any) {
if (error !== 'cancel') ElMessage.error(`${action.name}失败`)
}
}
</script>
<style scoped>
.devices-page { padding: 0; }
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
.header-actions { display: flex; align-items: center; }
</style>

View File

@ -16,10 +16,10 @@ public class CredentialsController : ControllerBase
} }
[HttpGet] [HttpGet]
public async Task<ActionResult<List<CredentialResponse>>> GetAllCredentials() public async Task<ActionResult<ApiResponse<List<CredentialResponse>>>> GetAllCredentials()
{ {
var credentials = await _credentialService.GetAllCredentialsAsync(); var credentials = await _credentialService.GetAllCredentialsAsync();
return credentials.Select(c => new CredentialResponse var result = credentials.Select(c => new CredentialResponse
{ {
Id = c.Id, Id = c.Id,
Name = c.Name, Name = c.Name,
@ -28,10 +28,11 @@ public class CredentialsController : ControllerBase
Description = c.Description, Description = c.Description,
HasPassword = !string.IsNullOrEmpty(c.Password) HasPassword = !string.IsNullOrEmpty(c.Password)
}).ToList(); }).ToList();
return Ok(ApiResponse<List<CredentialResponse>>.Success(result));
} }
[HttpPost] [HttpPost]
public async Task<ActionResult<CredentialResponse>> CreateCredential([FromBody] CredentialRequest request) public async Task<ActionResult<ApiResponse<CredentialResponse>>> CreateCredential([FromBody] CredentialRequest request)
{ {
var credential = new AmtCredential var credential = new AmtCredential
{ {
@ -44,7 +45,7 @@ public class CredentialsController : ControllerBase
var created = await _credentialService.CreateCredentialAsync(credential); var created = await _credentialService.CreateCredentialAsync(credential);
return CreatedAtAction(nameof(GetAllCredentials), new { id = created.Id }, new CredentialResponse var response = new CredentialResponse
{ {
Id = created.Id, Id = created.Id,
Name = created.Name, Name = created.Name,
@ -52,11 +53,12 @@ public class CredentialsController : ControllerBase
IsDefault = created.IsDefault, IsDefault = created.IsDefault,
Description = created.Description, Description = created.Description,
HasPassword = true HasPassword = true
}); };
return Ok(ApiResponse<CredentialResponse>.Success(response, "创建成功"));
} }
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<ActionResult<CredentialResponse>> UpdateCredential(long id, [FromBody] CredentialRequest request) public async Task<ActionResult<ApiResponse<CredentialResponse>>> UpdateCredential(long id, [FromBody] CredentialRequest request)
{ {
try try
{ {
@ -71,7 +73,7 @@ public class CredentialsController : ControllerBase
var updated = await _credentialService.UpdateCredentialAsync(id, credential); var updated = await _credentialService.UpdateCredentialAsync(id, credential);
return new CredentialResponse var response = new CredentialResponse
{ {
Id = updated.Id, Id = updated.Id,
Name = updated.Name, Name = updated.Name,
@ -80,18 +82,19 @@ public class CredentialsController : ControllerBase
Description = updated.Description, Description = updated.Description,
HasPassword = !string.IsNullOrEmpty(updated.Password) HasPassword = !string.IsNullOrEmpty(updated.Password)
}; };
return Ok(ApiResponse<CredentialResponse>.Success(response, "更新成功"));
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)
{ {
return NotFound(); return Ok(ApiResponse<CredentialResponse>.Fail(404, "凭据不存在"));
} }
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> DeleteCredential(long id) public async Task<ActionResult<ApiResponse<object>>> DeleteCredential(long id)
{ {
await _credentialService.DeleteCredentialAsync(id); await _credentialService.DeleteCredentialAsync(id);
return NoContent(); return Ok(ApiResponse<object>.Success(null, "删除成功"));
} }
} }

View File

@ -29,45 +29,166 @@ public class DevicesController : ControllerBase
} }
[HttpGet] [HttpGet]
public async Task<ActionResult<List<AmtDevice>>> GetAllDevices() public async Task<ActionResult<ApiResponse<List<AmtDevice>>>> GetAllDevices()
{ {
return await _context.AmtDevices.ToListAsync(); var devices = await _context.AmtDevices.ToListAsync();
return Ok(ApiResponse<List<AmtDevice>>.Success(devices));
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<ActionResult<AmtDevice>> GetDevice(long id) public async Task<ActionResult<ApiResponse<AmtDevice>>> GetDevice(long id)
{ {
var device = await _context.AmtDevices.FindAsync(id); var device = await _context.AmtDevices.FindAsync(id);
if (device == null) if (device == null)
{ {
return NotFound(); return Ok(ApiResponse<AmtDevice>.Fail(404, "设备不存在"));
} }
return device; return Ok(ApiResponse<AmtDevice>.Success(device));
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> DeleteDevice(long id) public async Task<ActionResult<ApiResponse<object>>> DeleteDevice(long id)
{ {
var device = await _context.AmtDevices.FindAsync(id); var device = await _context.AmtDevices.FindAsync(id);
if (device == null) if (device == null)
{ {
return NotFound(); return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
} }
_context.AmtDevices.Remove(device); _context.AmtDevices.Remove(device);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return NoContent(); return Ok(ApiResponse<object>.Success(null, "删除成功"));
}
/// <summary>
/// 手动添加设备
/// </summary>
[HttpPost]
public async Task<ActionResult<ApiResponse<AmtDevice>>> AddDevice([FromBody] AddDeviceRequest request)
{
// 验证 IP 地址格式
if (string.IsNullOrWhiteSpace(request.IpAddress))
{
return Ok(ApiResponse<AmtDevice>.Fail(400, "IP 地址不能为空"));
}
// 检查设备是否已存在
var existingDevice = await _context.AmtDevices.FirstOrDefaultAsync(d => d.IpAddress == request.IpAddress);
if (existingDevice != null)
{
return Ok(ApiResponse<AmtDevice>.Fail(400, $"设备 {request.IpAddress} 已存在"));
}
var device = new AmtDevice
{
IpAddress = request.IpAddress,
Hostname = request.Hostname,
Description = request.Description,
MajorVersion = 0,
MinorVersion = 0,
ProvisioningState = ProvisioningState.UNKNOWN,
AmtOnline = false,
OsOnline = false,
DiscoveredAt = DateTime.UtcNow,
LastSeenAt = DateTime.UtcNow
};
// 如果提供了 Windows 凭据,一并保存
if (!string.IsNullOrEmpty(request.WindowsUsername))
{
device.WindowsUsername = request.WindowsUsername;
if (!string.IsNullOrEmpty(request.WindowsPassword))
{
device.WindowsPassword = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.WindowsPassword));
}
}
_context.AmtDevices.Add(device);
await _context.SaveChangesAsync();
_logger.LogInformation("Manually added device {Ip}", request.IpAddress);
return Ok(ApiResponse<AmtDevice>.Success(device, "设备添加成功"));
}
/// <summary>
/// 更新设备信息
/// </summary>
[HttpPut("{id}")]
public async Task<ActionResult<ApiResponse<AmtDevice>>> UpdateDevice(long id, [FromBody] UpdateDeviceRequest request)
{
var device = await _context.AmtDevices.FindAsync(id);
if (device == null)
{
return Ok(ApiResponse<AmtDevice>.Fail(404, "设备不存在"));
}
if (!string.IsNullOrEmpty(request.Hostname))
device.Hostname = request.Hostname;
if (!string.IsNullOrEmpty(request.Description))
device.Description = request.Description;
await _context.SaveChangesAsync();
return Ok(ApiResponse<AmtDevice>.Success(device, "更新成功"));
}
/// <summary>
/// 设置设备的 Windows 登录凭据
/// </summary>
[HttpPut("{id}/credentials")]
public async Task<ActionResult<ApiResponse<object>>> SetDeviceCredentials(long id, [FromBody] SetDeviceCredentialsRequest request)
{
var device = await _context.AmtDevices.FindAsync(id);
if (device == null)
{
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
}
device.WindowsUsername = request.Username;
// 简单加密存储密码(生产环境应使用更安全的加密方式)
device.WindowsPassword = string.IsNullOrEmpty(request.Password) ? null :
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.Password));
await _context.SaveChangesAsync();
_logger.LogInformation("Updated Windows credentials for device {Id} ({Ip})", id, device.IpAddress);
return Ok(ApiResponse<object>.Success(null, "凭据设置成功"));
}
/// <summary>
/// 获取设备的 Windows 凭据(仅返回用户名,不返回密码)
/// </summary>
[HttpGet("{id}/credentials")]
public async Task<ActionResult<ApiResponse<DeviceCredentialsDto>>> GetDeviceCredentials(long id)
{
var device = await _context.AmtDevices.FindAsync(id);
if (device == null)
{
return Ok(ApiResponse<DeviceCredentialsDto>.Fail(404, "设备不存在"));
}
return Ok(ApiResponse<DeviceCredentialsDto>.Success(new DeviceCredentialsDto
{
DeviceId = device.Id,
Username = device.WindowsUsername,
HasPassword = !string.IsNullOrEmpty(device.WindowsPassword)
}));
} }
/// <summary> /// <summary>
/// 检测所有设备的在线状态 /// 检测所有设备的在线状态
/// </summary> /// </summary>
[HttpGet("status")] [HttpGet("status")]
public async Task<ActionResult<List<DeviceStatusDto>>> CheckAllDevicesStatus() public async Task<ActionResult<ApiResponse<List<DeviceStatusDto>>>> CheckAllDevicesStatus()
{ {
var devices = await _context.AmtDevices.ToListAsync(); var devices = await _context.AmtDevices.ToListAsync();
var credentials = await _context.AmtCredentials.ToListAsync(); var credentials = await _context.AmtCredentials.ToListAsync();
@ -107,20 +228,20 @@ public class DevicesController : ControllerBase
// 保存更新 // 保存更新
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return statusList; return Ok(ApiResponse<List<DeviceStatusDto>>.Success(statusList));
} }
/// <summary> /// <summary>
/// 检测单个设备的在线状态 /// 检测单个设备的在线状态
/// </summary> /// </summary>
[HttpGet("{id}/status")] [HttpGet("{id}/status")]
public async Task<ActionResult<DeviceStatusDto>> CheckDeviceStatus(long id) public async Task<ActionResult<ApiResponse<DeviceStatusDto>>> CheckDeviceStatus(long id)
{ {
var device = await _context.AmtDevices.FindAsync(id); var device = await _context.AmtDevices.FindAsync(id);
if (device == null) if (device == null)
{ {
return NotFound(); return Ok(ApiResponse<DeviceStatusDto>.Fail(404, "设备不存在"));
} }
var credentials = await _context.AmtCredentials.ToListAsync(); var credentials = await _context.AmtCredentials.ToListAsync();
@ -142,13 +263,13 @@ public class DevicesController : ControllerBase
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return new DeviceStatusDto return Ok(ApiResponse<DeviceStatusDto>.Success(new DeviceStatusDto
{ {
Id = device.Id, Id = device.Id,
IpAddress = device.IpAddress, IpAddress = device.IpAddress,
AmtOnline = amtOnline, AmtOnline = amtOnline,
OsOnline = osOnline OsOnline = osOnline
}; }));
} }
/// <summary> /// <summary>
@ -246,3 +367,43 @@ public class DeviceStatusDto
public bool AmtOnline { get; set; } public bool AmtOnline { get; set; }
public bool OsOnline { get; set; } public bool OsOnline { get; set; }
} }
/// <summary>
/// 更新设备请求
/// </summary>
public class UpdateDeviceRequest
{
public string? Hostname { get; set; }
public string? Description { get; set; }
}
/// <summary>
/// 设置设备 Windows 凭据请求
/// </summary>
public class SetDeviceCredentialsRequest
{
public string? Username { get; set; }
public string? Password { get; set; }
}
/// <summary>
/// 设备凭据DTO
/// </summary>
public class DeviceCredentialsDto
{
public long DeviceId { get; set; }
public string? Username { get; set; }
public bool HasPassword { get; set; }
}
/// <summary>
/// 添加设备请求
/// </summary>
public class AddDeviceRequest
{
public string IpAddress { get; set; } = string.Empty;
public string? Hostname { get; set; }
public string? Description { get; set; }
public string? WindowsUsername { get; set; }
public string? WindowsPassword { get; set; }
}

View File

@ -20,24 +20,24 @@ public class HardwareInfoController : ControllerBase
} }
[HttpGet("{deviceId}")] [HttpGet("{deviceId}")]
public async Task<ActionResult<HardwareInfoDto>> GetHardwareInfo( public async Task<ActionResult<ApiResponse<HardwareInfoDto>>> GetHardwareInfo(
long deviceId, long deviceId,
[FromQuery] bool refresh = false) [FromQuery] bool refresh = false)
{ {
try try
{ {
var result = await _hardwareInfoService.GetHardwareInfoAsync(deviceId, refresh); var result = await _hardwareInfoService.GetHardwareInfoAsync(deviceId, refresh);
return Ok(result); return Ok(ApiResponse<HardwareInfoDto>.Success(result));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting hardware info for device {DeviceId}", deviceId); _logger.LogError(ex, "Error getting hardware info for device {DeviceId}", deviceId);
return StatusCode(500, new { error = ex.Message }); return Ok(ApiResponse<HardwareInfoDto>.Fail(500, ex.Message));
} }
} }
[HttpPost("batch")] [HttpPost("batch")]
public async Task<ActionResult<List<BatchHardwareInfoResult>>> GetBatchHardwareInfo( public async Task<ActionResult<ApiResponse<BatchHardwareInfoResponse>>> GetBatchHardwareInfo(
[FromBody] BatchHardwareInfoRequest request) [FromBody] BatchHardwareInfoRequest request)
{ {
try try
@ -45,12 +45,17 @@ public class HardwareInfoController : ControllerBase
var results = await _hardwareInfoService.GetBatchHardwareInfoAsync( var results = await _hardwareInfoService.GetBatchHardwareInfoAsync(
request.DeviceIds, request.DeviceIds,
request.Refresh); request.Refresh);
return Ok(new { results }); return Ok(ApiResponse<BatchHardwareInfoResponse>.Success(new BatchHardwareInfoResponse { Results = results }));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting batch hardware info"); _logger.LogError(ex, "Error getting batch hardware info");
return StatusCode(500, new { error = ex.Message }); return Ok(ApiResponse<BatchHardwareInfoResponse>.Fail(500, ex.Message));
} }
} }
} }
public class BatchHardwareInfoResponse
{
public List<BatchHardwareInfoResult> Results { get; set; } = new();
}

View File

@ -1,4 +1,5 @@
using AmtScanner.Api.Data; using AmtScanner.Api.Data;
using AmtScanner.Api.Models;
using AmtScanner.Api.Services; using AmtScanner.Api.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -30,18 +31,18 @@ public class PowerController : ControllerBase
/// 获取设备电源状态 /// 获取设备电源状态
/// </summary> /// </summary>
[HttpGet("{deviceId}/state")] [HttpGet("{deviceId}/state")]
public async Task<ActionResult<PowerStateResponse>> GetPowerState(long deviceId) public async Task<ActionResult<ApiResponse<PowerStateResponse>>> GetPowerState(long deviceId)
{ {
var device = await _context.AmtDevices.FindAsync(deviceId); var device = await _context.AmtDevices.FindAsync(deviceId);
if (device == null) if (device == null)
{ {
return NotFound(new { error = "设备不存在" }); return Ok(ApiResponse<PowerStateResponse>.Fail(404, "设备不存在"));
} }
var credential = await _credentialService.GetDefaultCredentialAsync(); var credential = await _credentialService.GetDefaultCredentialAsync();
if (credential == null) if (credential == null)
{ {
return BadRequest(new { error = "没有配置默认凭据" }); return Ok(ApiResponse<PowerStateResponse>.Fail(400, "没有配置默认凭据"));
} }
var openPorts = new List<int>(); var openPorts = new List<int>();
@ -53,13 +54,13 @@ public class PowerController : ControllerBase
if (openPorts.Count == 0) if (openPorts.Count == 0)
{ {
return Ok(new PowerStateResponse return Ok(ApiResponse<PowerStateResponse>.Success(new PowerStateResponse
{ {
DeviceId = deviceId, DeviceId = deviceId,
IpAddress = device.IpAddress, IpAddress = device.IpAddress,
Success = false, Success = false,
Error = "设备离线或AMT端口不可用" Error = "设备离线或AMT端口不可用"
}); }));
} }
// 解密密码 // 解密密码
@ -71,7 +72,7 @@ public class PowerController : ControllerBase
decryptedPassword, decryptedPassword,
openPorts); openPorts);
return Ok(new PowerStateResponse return Ok(ApiResponse<PowerStateResponse>.Success(new PowerStateResponse
{ {
DeviceId = deviceId, DeviceId = deviceId,
IpAddress = device.IpAddress, IpAddress = device.IpAddress,
@ -79,14 +80,14 @@ public class PowerController : ControllerBase
PowerState = result.PowerState, PowerState = result.PowerState,
PowerStateText = result.PowerStateText, PowerStateText = result.PowerStateText,
Error = result.Error Error = result.Error
}); }));
} }
/// <summary> /// <summary>
/// 开机 /// 开机
/// </summary> /// </summary>
[HttpPost("{deviceId}/power-on")] [HttpPost("{deviceId}/power-on")]
public async Task<ActionResult<PowerActionResponse>> PowerOn(long deviceId) public async Task<ActionResult<ApiResponse<PowerActionResponse>>> PowerOn(long deviceId)
{ {
return await ExecutePowerAction(deviceId, PowerAction.PowerOn); return await ExecutePowerAction(deviceId, PowerAction.PowerOn);
} }
@ -95,7 +96,7 @@ public class PowerController : ControllerBase
/// 关机(优雅关机) /// 关机(优雅关机)
/// </summary> /// </summary>
[HttpPost("{deviceId}/power-off")] [HttpPost("{deviceId}/power-off")]
public async Task<ActionResult<PowerActionResponse>> PowerOff(long deviceId) public async Task<ActionResult<ApiResponse<PowerActionResponse>>> PowerOff(long deviceId)
{ {
return await ExecutePowerAction(deviceId, PowerAction.GracefulOff); return await ExecutePowerAction(deviceId, PowerAction.GracefulOff);
} }
@ -104,7 +105,7 @@ public class PowerController : ControllerBase
/// 强制关机 /// 强制关机
/// </summary> /// </summary>
[HttpPost("{deviceId}/force-off")] [HttpPost("{deviceId}/force-off")]
public async Task<ActionResult<PowerActionResponse>> ForceOff(long deviceId) public async Task<ActionResult<ApiResponse<PowerActionResponse>>> ForceOff(long deviceId)
{ {
return await ExecutePowerAction(deviceId, PowerAction.PowerOff); return await ExecutePowerAction(deviceId, PowerAction.PowerOff);
} }
@ -113,7 +114,7 @@ public class PowerController : ControllerBase
/// 重启(优雅重启) /// 重启(优雅重启)
/// </summary> /// </summary>
[HttpPost("{deviceId}/restart")] [HttpPost("{deviceId}/restart")]
public async Task<ActionResult<PowerActionResponse>> Restart(long deviceId) public async Task<ActionResult<ApiResponse<PowerActionResponse>>> Restart(long deviceId)
{ {
return await ExecutePowerAction(deviceId, PowerAction.GracefulReset); return await ExecutePowerAction(deviceId, PowerAction.GracefulReset);
} }
@ -122,7 +123,7 @@ public class PowerController : ControllerBase
/// 强制重启 /// 强制重启
/// </summary> /// </summary>
[HttpPost("{deviceId}/force-restart")] [HttpPost("{deviceId}/force-restart")]
public async Task<ActionResult<PowerActionResponse>> ForceRestart(long deviceId) public async Task<ActionResult<ApiResponse<PowerActionResponse>>> ForceRestart(long deviceId)
{ {
return await ExecutePowerAction(deviceId, PowerAction.Reset); return await ExecutePowerAction(deviceId, PowerAction.Reset);
} }
@ -131,23 +132,23 @@ public class PowerController : ControllerBase
/// 电源循环(硬重启) /// 电源循环(硬重启)
/// </summary> /// </summary>
[HttpPost("{deviceId}/power-cycle")] [HttpPost("{deviceId}/power-cycle")]
public async Task<ActionResult<PowerActionResponse>> PowerCycle(long deviceId) public async Task<ActionResult<ApiResponse<PowerActionResponse>>> PowerCycle(long deviceId)
{ {
return await ExecutePowerAction(deviceId, PowerAction.PowerCycle); return await ExecutePowerAction(deviceId, PowerAction.PowerCycle);
} }
private async Task<ActionResult<PowerActionResponse>> ExecutePowerAction(long deviceId, PowerAction action) private async Task<ActionResult<ApiResponse<PowerActionResponse>>> ExecutePowerAction(long deviceId, PowerAction action)
{ {
var device = await _context.AmtDevices.FindAsync(deviceId); var device = await _context.AmtDevices.FindAsync(deviceId);
if (device == null) if (device == null)
{ {
return NotFound(new { error = "设备不存在" }); return Ok(ApiResponse<PowerActionResponse>.Fail(404, "设备不存在"));
} }
var credential = await _credentialService.GetDefaultCredentialAsync(); var credential = await _credentialService.GetDefaultCredentialAsync();
if (credential == null) if (credential == null)
{ {
return BadRequest(new { error = "没有配置默认凭据" }); return Ok(ApiResponse<PowerActionResponse>.Fail(400, "没有配置默认凭据"));
} }
// 检测可用端口 // 检测可用端口
@ -155,14 +156,14 @@ public class PowerController : ControllerBase
if (openPorts.Count == 0) if (openPorts.Count == 0)
{ {
return Ok(new PowerActionResponse return Ok(ApiResponse<PowerActionResponse>.Success(new PowerActionResponse
{ {
DeviceId = deviceId, DeviceId = deviceId,
IpAddress = device.IpAddress, IpAddress = device.IpAddress,
Action = action.ToString(), Action = action.ToString(),
Success = false, Success = false,
Error = "AMT端口不可用" Error = "AMT端口不可用"
}); }));
} }
// 解密密码 // 解密密码
@ -175,7 +176,7 @@ public class PowerController : ControllerBase
openPorts, openPorts,
action); action);
return Ok(new PowerActionResponse return Ok(ApiResponse<PowerActionResponse>.Success(new PowerActionResponse
{ {
DeviceId = deviceId, DeviceId = deviceId,
IpAddress = device.IpAddress, IpAddress = device.IpAddress,
@ -183,7 +184,7 @@ public class PowerController : ControllerBase
Success = result.Success, Success = result.Success,
Message = result.Message, Message = result.Message,
Error = result.Error Error = result.Error
}); }));
} }
private async Task<List<int>> DetectOpenPortsAsync(string ipAddress) private async Task<List<int>> DetectOpenPortsAsync(string ipAddress)

View File

@ -1,4 +1,4 @@
using AmtScanner.Api.Data; using AmtScanner.Api.Data;
using AmtScanner.Api.Models; using AmtScanner.Api.Models;
using AmtScanner.Api.Services; using AmtScanner.Api.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -8,7 +8,7 @@ using System.Security.Cryptography;
namespace AmtScanner.Api.Controllers; namespace AmtScanner.Api.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/remote-desktop")]
public class RemoteDesktopController : ControllerBase public class RemoteDesktopController : ControllerBase
{ {
private readonly IGuacamoleService _guacamoleService; private readonly IGuacamoleService _guacamoleService;
@ -25,6 +25,15 @@ public class RemoteDesktopController : ControllerBase
_logger = logger; _logger = logger;
} }
/// <summary>
/// 健康检查
/// </summary>
[HttpGet("health")]
public IActionResult Health()
{
return Ok(ApiResponse<object>.Success(new { status = "ok" }, "远程桌面服务正常"));
}
/// <summary> /// <summary>
/// 生成远程访问 Token管理员使用 /// 生成远程访问 Token管理员使用
/// </summary> /// </summary>
@ -37,18 +46,9 @@ public class RemoteDesktopController : ControllerBase
if (device == null) if (device == null)
return Ok(ApiResponse<GenerateTokenResponse>.Fail(404, "设备不存在")); return Ok(ApiResponse<GenerateTokenResponse>.Fail(404, "设备不存在"));
WindowsCredential? credential = null; // 检查设备是否配置了 Windows 凭据
if (request.CredentialId.HasValue) if (string.IsNullOrEmpty(device.WindowsUsername) || string.IsNullOrEmpty(device.WindowsPassword))
{ return Ok(ApiResponse<GenerateTokenResponse>.Fail(400, "请先为该设备配置 Windows 登录凭据"));
credential = await _context.WindowsCredentials.FindAsync(request.CredentialId.Value);
}
else
{
credential = await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault);
}
if (credential == null)
return Ok(ApiResponse<GenerateTokenResponse>.Fail(400, "请先配置 Windows 凭据"));
var token = GenerateRandomToken(); var token = GenerateRandomToken();
var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30); var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30);
@ -57,7 +57,6 @@ public class RemoteDesktopController : ControllerBase
{ {
Token = token, Token = token,
DeviceId = deviceId, DeviceId = deviceId,
WindowsCredentialId = credential.Id,
ExpiresAt = expiresAt, ExpiresAt = expiresAt,
MaxUseCount = request.MaxUseCount ?? 1, MaxUseCount = request.MaxUseCount ?? 1,
Note = request.Note Note = request.Note
@ -67,7 +66,7 @@ public class RemoteDesktopController : ControllerBase
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var baseUrl = $"{Request.Scheme}://{Request.Host}"; var baseUrl = $"{Request.Scheme}://{Request.Host}";
var accessUrl = $"{baseUrl}/remote/{token}"; var accessUrl = $"{baseUrl}/#/remote/{token}";
_logger.LogInformation("Generated remote access token for device {Ip}, expires at {ExpiresAt}", _logger.LogInformation("Generated remote access token for device {Ip}, expires at {ExpiresAt}",
device.IpAddress, expiresAt); device.IpAddress, expiresAt);
@ -91,7 +90,6 @@ public class RemoteDesktopController : ControllerBase
{ {
var accessToken = await _context.RemoteAccessTokens var accessToken = await _context.RemoteAccessTokens
.Include(t => t.Device) .Include(t => t.Device)
.Include(t => t.WindowsCredential)
.FirstOrDefaultAsync(t => t.Token == token); .FirstOrDefaultAsync(t => t.Token == token);
if (accessToken == null) if (accessToken == null)
@ -100,8 +98,13 @@ public class RemoteDesktopController : ControllerBase
if (!accessToken.IsValid()) if (!accessToken.IsValid())
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "访问链接已过期或已达到使用次数上限")); return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "访问链接已过期或已达到使用次数上限"));
if (accessToken.Device == null || accessToken.WindowsCredential == null) if (accessToken.Device == null)
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "设备或凭据信息不完整")); return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "设备信息不完整"));
if (string.IsNullOrEmpty(accessToken.Device.WindowsUsername) || string.IsNullOrEmpty(accessToken.Device.WindowsPassword))
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "设备未配置 Windows 登录凭据"));
var password = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(accessToken.Device.WindowsPassword));
accessToken.UseCount++; accessToken.UseCount++;
accessToken.UsedAt = DateTime.UtcNow; accessToken.UsedAt = DateTime.UtcNow;
@ -114,7 +117,7 @@ public class RemoteDesktopController : ControllerBase
var connectionName = $"AMT-{accessToken.Device.IpAddress}"; var connectionName = $"AMT-{accessToken.Device.IpAddress}";
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync( var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(
guacToken, connectionName, accessToken.Device.IpAddress, guacToken, connectionName, accessToken.Device.IpAddress,
accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password); accessToken.Device.WindowsUsername!, password);
if (string.IsNullOrEmpty(connectionId)) if (string.IsNullOrEmpty(connectionId))
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(500, "创建远程连接失败")); return Ok(ApiResponse<RemoteDesktopResponse>.Fail(500, "创建远程连接失败"));
@ -131,6 +134,7 @@ public class RemoteDesktopController : ControllerBase
})); }));
} }
/// <summary> /// <summary>
/// 验证 Token 是否有效 /// 验证 Token 是否有效
/// </summary> /// </summary>
@ -204,9 +208,10 @@ public class RemoteDesktopController : ControllerBase
var count = await _context.RemoteAccessTokens var count = await _context.RemoteAccessTokens
.Where(t => t.ExpiresAt < DateTime.UtcNow) .Where(t => t.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
return Ok(ApiResponse<CleanupTokensResponse>.Success(new CleanupTokensResponse { DeletedCount = count }, "已清理 " + count + " 个过期 Token")); return Ok(ApiResponse<CleanupTokensResponse>.Success(new CleanupTokensResponse { DeletedCount = count }, $"已清理 {count} 个过期 Token"));
} }
/// <summary> /// <summary>
/// 直接连接(需要输入凭据) /// 直接连接(需要输入凭据)
/// </summary> /// </summary>
@ -264,7 +269,6 @@ public class RemoteDesktopController : ControllerBase
public class GenerateTokenRequest public class GenerateTokenRequest
{ {
public long? CredentialId { get; set; }
public int? ExpiresInMinutes { get; set; } = 30; public int? ExpiresInMinutes { get; set; } = 30;
public int? MaxUseCount { get; set; } = 1; public int? MaxUseCount { get; set; } = 1;
public string? Note { get; set; } public string? Note { get; set; }

View File

@ -1,6 +1,8 @@
using AmtScanner.Api.Models;
using AmtScanner.Api.Services; using AmtScanner.Api.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
namespace AmtScanner.Api.Controllers; namespace AmtScanner.Api.Controllers;
@ -12,6 +14,9 @@ public class ScanController : ControllerBase
private readonly IHubContext<ScanProgressHub> _hubContext; private readonly IHubContext<ScanProgressHub> _hubContext;
private readonly ILogger<ScanController> _logger; private readonly ILogger<ScanController> _logger;
// 存储扫描进度状态
private static readonly ConcurrentDictionary<string, ScanStatusInfo> _scanStatuses = new();
public ScanController( public ScanController(
IAmtScannerService scannerService, IAmtScannerService scannerService,
IHubContext<ScanProgressHub> hubContext, IHubContext<ScanProgressHub> hubContext,
@ -23,17 +28,36 @@ public class ScanController : ControllerBase
} }
[HttpPost("start")] [HttpPost("start")]
public async Task<IActionResult> StartScan([FromBody] ScanRequest request) public async Task<ActionResult<ApiResponse<ScanStartResponse>>> StartScan([FromBody] ScanRequest request)
{ {
var taskId = Guid.NewGuid().ToString(); var taskId = Guid.NewGuid().ToString();
_logger.LogInformation("Starting scan task: {TaskId}", taskId); _logger.LogInformation("Starting scan task: {TaskId}", taskId);
// 初始化扫描状态
_scanStatuses[taskId] = new ScanStatusInfo
{
TaskId = taskId,
Status = "running",
ScannedCount = 0,
TotalCount = 0,
FoundDevices = 0
};
// Start scan in background // Start scan in background
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
var progress = new Progress<ScanProgress>(async p => var progress = new Progress<ScanProgress>(async p =>
{ {
// 更新状态存储
if (_scanStatuses.TryGetValue(taskId, out var status))
{
status.ScannedCount = p.ScannedCount;
status.TotalCount = p.TotalCount;
status.FoundDevices = p.FoundDevices;
status.CurrentIp = p.CurrentIp;
}
await _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p); await _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p);
}); });
@ -46,26 +70,73 @@ public class ScanController : ControllerBase
progress progress
); );
// 更新状态为完成
if (_scanStatuses.TryGetValue(taskId, out var status))
{
status.Status = "completed";
}
// Send completion notification // Send completion notification
_logger.LogInformation("Scan task {TaskId} completed", taskId); _logger.LogInformation("Scan task {TaskId} completed", taskId);
await _hubContext.Clients.All.SendAsync("ScanCompleted", new { taskId }); await _hubContext.Clients.All.SendAsync("ScanCompleted", new { taskId });
} }
catch (Exception ex) catch (Exception ex)
{ {
// 更新状态为错误
if (_scanStatuses.TryGetValue(taskId, out var status))
{
status.Status = "error";
status.Error = ex.Message;
}
_logger.LogError(ex, "Error in scan task {TaskId}", taskId); _logger.LogError(ex, "Error in scan task {TaskId}", taskId);
await _hubContext.Clients.All.SendAsync("ScanError", new { taskId, error = ex.Message }); await _hubContext.Clients.All.SendAsync("ScanError", new { taskId, error = ex.Message });
} }
}); });
return Ok(new { taskId }); return Ok(ApiResponse<ScanStartResponse>.Success(new ScanStartResponse { TaskId = taskId }, "扫描任务已启动"));
}
[HttpGet("status/{taskId}")]
public ActionResult<ApiResponse<ScanStatusInfo>> GetScanStatus(string taskId)
{
if (_scanStatuses.TryGetValue(taskId, out var status))
{
return Ok(ApiResponse<ScanStatusInfo>.Success(status));
}
return Ok(ApiResponse<ScanStatusInfo>.Fail(404, "扫描任务不存在"));
} }
[HttpPost("cancel/{taskId}")] [HttpPost("cancel/{taskId}")]
public IActionResult CancelScan(string taskId) public ActionResult<ApiResponse<object>> CancelScan(string taskId)
{ {
_scannerService.CancelScan(taskId); _scannerService.CancelScan(taskId);
return Ok();
// 更新状态为已取消
if (_scanStatuses.TryGetValue(taskId, out var status))
{
status.Status = "cancelled";
} }
return Ok(ApiResponse<object>.Success(null, "扫描任务已取消"));
}
}
public class ScanStartResponse
{
public string TaskId { get; set; } = string.Empty;
}
public class ScanStatusInfo
{
public string TaskId { get; set; } = string.Empty;
public string Status { get; set; } = "idle"; // idle, running, completed, cancelled, error
public int ScannedCount { get; set; }
public int TotalCount { get; set; }
public int FoundDevices { get; set; }
public string? CurrentIp { get; set; }
public string? Error { get; set; }
} }
public class ScanRequest public class ScanRequest

View File

@ -22,7 +22,7 @@ public class WindowsCredentialsController : ControllerBase
/// 获取所有 Windows 凭据 /// 获取所有 Windows 凭据
/// </summary> /// </summary>
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<WindowsCredentialDto>>> GetAll() public async Task<ActionResult<ApiResponse<List<WindowsCredentialDto>>>> GetAll()
{ {
var credentials = await _context.WindowsCredentials var credentials = await _context.WindowsCredentials
.OrderByDescending(c => c.IsDefault) .OrderByDescending(c => c.IsDefault)
@ -39,14 +39,14 @@ public class WindowsCredentialsController : ControllerBase
}) })
.ToListAsync(); .ToListAsync();
return Ok(credentials); return Ok(ApiResponse<List<WindowsCredentialDto>>.Success(credentials));
} }
/// <summary> /// <summary>
/// 创建 Windows 凭据 /// 创建 Windows 凭据
/// </summary> /// </summary>
[HttpPost] [HttpPost]
public async Task<ActionResult<WindowsCredentialDto>> Create([FromBody] CreateWindowsCredentialRequest request) public async Task<ActionResult<ApiResponse<WindowsCredentialDto>>> Create([FromBody] CreateWindowsCredentialRequest request)
{ {
// 如果设为默认,取消其他默认 // 如果设为默认,取消其他默认
if (request.IsDefault) if (request.IsDefault)
@ -71,7 +71,7 @@ public class WindowsCredentialsController : ControllerBase
_logger.LogInformation("Created Windows credential: {Name}", credential.Name); _logger.LogInformation("Created Windows credential: {Name}", credential.Name);
return Ok(new WindowsCredentialDto var dto = new WindowsCredentialDto
{ {
Id = credential.Id, Id = credential.Id,
Name = credential.Name, Name = credential.Name,
@ -80,19 +80,21 @@ public class WindowsCredentialsController : ControllerBase
IsDefault = credential.IsDefault, IsDefault = credential.IsDefault,
Note = credential.Note, Note = credential.Note,
CreatedAt = credential.CreatedAt CreatedAt = credential.CreatedAt
}); };
return Ok(ApiResponse<WindowsCredentialDto>.Success(dto, "创建成功"));
} }
/// <summary> /// <summary>
/// 更新 Windows 凭据 /// 更新 Windows 凭据
/// </summary> /// </summary>
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<ActionResult> Update(long id, [FromBody] UpdateWindowsCredentialRequest request) public async Task<ActionResult<ApiResponse<object>>> Update(long id, [FromBody] UpdateWindowsCredentialRequest request)
{ {
var credential = await _context.WindowsCredentials.FindAsync(id); var credential = await _context.WindowsCredentials.FindAsync(id);
if (credential == null) if (credential == null)
{ {
return NotFound(new { error = "凭据不存在" }); return Ok(ApiResponse<object>.Fail(404, "凭据不存在"));
} }
// 如果设为默认,取消其他默认 // 如果设为默认,取消其他默认
@ -116,19 +118,19 @@ public class WindowsCredentialsController : ControllerBase
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return Ok(new { success = true }); return Ok(ApiResponse<object>.Success(null, "更新成功"));
} }
/// <summary> /// <summary>
/// 删除 Windows 凭据 /// 删除 Windows 凭据
/// </summary> /// </summary>
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<ActionResult> Delete(long id) public async Task<ActionResult<ApiResponse<object>>> Delete(long id)
{ {
var credential = await _context.WindowsCredentials.FindAsync(id); var credential = await _context.WindowsCredentials.FindAsync(id);
if (credential == null) if (credential == null)
{ {
return NotFound(new { error = "凭据不存在" }); return Ok(ApiResponse<object>.Fail(404, "凭据不存在"));
} }
_context.WindowsCredentials.Remove(credential); _context.WindowsCredentials.Remove(credential);
@ -136,19 +138,19 @@ public class WindowsCredentialsController : ControllerBase
_logger.LogInformation("Deleted Windows credential: {Name}", credential.Name); _logger.LogInformation("Deleted Windows credential: {Name}", credential.Name);
return Ok(new { success = true }); return Ok(ApiResponse<object>.Success(null, "删除成功"));
} }
/// <summary> /// <summary>
/// 设置默认凭据 /// 设置默认凭据
/// </summary> /// </summary>
[HttpPost("{id}/set-default")] [HttpPost("{id}/set-default")]
public async Task<ActionResult> SetDefault(long id) public async Task<ActionResult<ApiResponse<object>>> SetDefault(long id)
{ {
var credential = await _context.WindowsCredentials.FindAsync(id); var credential = await _context.WindowsCredentials.FindAsync(id);
if (credential == null) if (credential == null)
{ {
return NotFound(new { error = "凭据不存在" }); return Ok(ApiResponse<object>.Fail(404, "凭据不存在"));
} }
// 取消其他默认 // 取消其他默认
@ -159,7 +161,7 @@ public class WindowsCredentialsController : ControllerBase
credential.IsDefault = true; credential.IsDefault = true;
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return Ok(new { success = true }); return Ok(ApiResponse<object>.Success(null, "设置成功"));
} }
} }

View File

@ -95,12 +95,6 @@ public class AppDbContext : DbContext
.HasForeignKey(t => t.DeviceId) .HasForeignKey(t => t.DeviceId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RemoteAccessToken>()
.HasOne(t => t.WindowsCredential)
.WithMany()
.HasForeignKey(t => t.WindowsCredentialId)
.OnDelete(DeleteBehavior.SetNull);
// User 配置 // User 配置
modelBuilder.Entity<User>() modelBuilder.Entity<User>()
.Property(u => u.UserName) .Property(u => u.UserName)

View File

@ -101,18 +101,21 @@ public static class DbSeeder
var menus = new List<Menu> var menus = new List<Menu>
{ {
// 仪表盘菜单 - 与前端 dashboard.ts 匹配(系统内置) // 仪表盘菜单(系统内置)
new() { Id = 1, Name = "Dashboard", Path = "/dashboard", Component = "/index/index", Title = "menus.dashboard.title", Icon = "ri:pie-chart-line", Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true }, new() { Id = 1, Name = "Dashboard", Path = "/dashboard", Component = "/index/index", Title = "menus.dashboard.title", Icon = "ri:pie-chart-line", Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true }, new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
// AMT 设备管理菜单(系统内置) // AMT 设备管理菜单(系统内置)
new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "AMT设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "网络扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "设备扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备列表", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备管理", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 8, ParentId = 5, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 8, ParentId = 5, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 9, ParentId = 5, Name = "WindowsCredentials", Path = "windows-credentials", Component = "/amt/windows-credentials", Title = "Windows凭据", KeepAlive = true, Sort = 4, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
// 系统管理菜单 - 与前端 system.ts 匹配(系统内置) // 桌面管理菜单(系统内置)
new() { Id = 20, Name = "DesktopManage", Path = "/desktop-manage", Component = "/index/index", Title = "桌面管理", Icon = "ri:remote-control-line", Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 21, ParentId = 20, Name = "DesktopDevices", Path = "devices", Component = "/desktop-manage/devices", Title = "远程桌面", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
// 系统管理菜单(系统内置)
new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 11, ParentId = 10, Name = "User", Path = "user", Component = "/system/user", Title = "menus.system.user", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true }, new() { Id = 11, ParentId = 10, Name = "User", Path = "user", Component = "/system/user", Title = "menus.system.user", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
new() { Id = 12, ParentId = 10, Name = "Role", Path = "role", Component = "/system/role", Title = "menus.system.role", KeepAlive = true, Sort = 2, Roles = "R_SUPER", IsSystem = true }, new() { Id = 12, ParentId = 10, Name = "Role", Path = "role", Component = "/system/role", Title = "menus.system.role", KeepAlive = true, Sort = 2, Roles = "R_SUPER", IsSystem = true },

View File

@ -0,0 +1,656 @@
// <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("20260120122638_AddDeviceWindowsCredentials")]
partial class AddDeviceWindowsCredentials
{
/// <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>("WindowsPassword")
.HasColumnType("longtext");
b.Property<string>("WindowsUsername")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("IpAddress")
.IsUnique();
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.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.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,99 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AmtScanner.Api.Migrations
{
/// <inheritdoc />
public partial class AddDeviceWindowsCredentials : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 使用原生 SQL 安全删除外键和索引(如果存在)
migrationBuilder.Sql(@"
SET @fk_exists = (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'RemoteAccessTokens'
AND CONSTRAINT_NAME = 'FK_RemoteAccessTokens_WindowsCredentials_WindowsCredentialId');
SET @sql = IF(@fk_exists > 0,
'ALTER TABLE RemoteAccessTokens DROP FOREIGN KEY FK_RemoteAccessTokens_WindowsCredentials_WindowsCredentialId',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
");
migrationBuilder.Sql(@"
SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'RemoteAccessTokens'
AND INDEX_NAME = 'IX_RemoteAccessTokens_WindowsCredentialId');
SET @sql = IF(@idx_exists > 0,
'ALTER TABLE RemoteAccessTokens DROP INDEX IX_RemoteAccessTokens_WindowsCredentialId',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
");
migrationBuilder.Sql(@"
SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'RemoteAccessTokens'
AND COLUMN_NAME = 'WindowsCredentialId');
SET @sql = IF(@col_exists > 0,
'ALTER TABLE RemoteAccessTokens DROP COLUMN WindowsCredentialId',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
");
migrationBuilder.AddColumn<string>(
name: "WindowsPassword",
table: "AmtDevices",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "WindowsUsername",
table: "AmtDevices",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WindowsPassword",
table: "AmtDevices");
migrationBuilder.DropColumn(
name: "WindowsUsername",
table: "AmtDevices");
migrationBuilder.AddColumn<long>(
name: "WindowsCredentialId",
table: "RemoteAccessTokens",
type: "bigint",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_RemoteAccessTokens_WindowsCredentialId",
table: "RemoteAccessTokens",
column: "WindowsCredentialId");
migrationBuilder.AddForeignKey(
name: "FK_RemoteAccessTokens_WindowsCredentials_WindowsCredentialId",
table: "RemoteAccessTokens",
column: "WindowsCredentialId",
principalTable: "WindowsCredentials",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}

View File

@ -95,6 +95,12 @@ namespace AmtScanner.Api.Migrations
b.Property<int>("ProvisioningState") b.Property<int>("ProvisioningState")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("WindowsPassword")
.HasColumnType("longtext");
b.Property<string>("WindowsUsername")
.HasColumnType("longtext");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IpAddress") b.HasIndex("IpAddress")
@ -303,9 +309,6 @@ namespace AmtScanner.Api.Migrations
b.Property<DateTime?>("UsedAt") b.Property<DateTime?>("UsedAt")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<long?>("WindowsCredentialId")
.HasColumnType("bigint");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("DeviceId"); b.HasIndex("DeviceId");
@ -313,8 +316,6 @@ namespace AmtScanner.Api.Migrations
b.HasIndex("Token") b.HasIndex("Token")
.IsUnique(); .IsUnique();
b.HasIndex("WindowsCredentialId");
b.ToTable("RemoteAccessTokens"); b.ToTable("RemoteAccessTokens");
}); });
@ -569,14 +570,7 @@ namespace AmtScanner.Api.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("AmtScanner.Api.Models.WindowsCredential", "WindowsCredential")
.WithMany()
.HasForeignKey("WindowsCredentialId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Device"); b.Navigation("Device");
b.Navigation("WindowsCredential");
}); });
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b => modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>

View File

@ -30,6 +30,16 @@ public class AmtDevice
/// </summary> /// </summary>
public bool OsOnline { get; set; } public bool OsOnline { get; set; }
/// <summary>
/// Windows 登录用户名
/// </summary>
public string? WindowsUsername { get; set; }
/// <summary>
/// Windows 登录密码(加密存储)
/// </summary>
public string? WindowsPassword { get; set; }
public DateTime DiscoveredAt { get; set; } public DateTime DiscoveredAt { get; set; }
public DateTime LastSeenAt { get; set; } public DateTime LastSeenAt { get; set; }

View File

@ -27,16 +27,6 @@ public class RemoteAccessToken
/// </summary> /// </summary>
public AmtDevice? Device { get; set; } public AmtDevice? Device { get; set; }
/// <summary>
/// 关联的 Windows 凭据 ID
/// </summary>
public long? WindowsCredentialId { get; set; }
/// <summary>
/// 关联的 Windows 凭据
/// </summary>
public WindowsCredential? WindowsCredential { get; set; }
/// <summary> /// <summary>
/// 创建时间 /// 创建时间
/// </summary> /// </summary>

View File

@ -0,0 +1,25 @@
-- 添加桌面管理菜单
-- 数据库: amtscanner
-- 已执行: 2026-01-20
-- 添加一级菜单:桌面管理
INSERT INTO `Menus` (`Id`, `ParentId`, `Name`, `Path`, `Component`, `Title`, `Icon`, `Sort`, `Roles`, `IsSystem`, `IsHide`, `KeepAlive`, `IsIframe`, `IsHideTab`, `CreatedAt`)
VALUES (20, NULL, 'DesktopManage', '/desktop-manage', '/index/index', '桌面管理', 'ri:remote-control-line', 3, 'R_SUPER,R_ADMIN', 1, 0, 0, 0, 0, NOW());
-- 添加二级菜单:远程桌面
INSERT INTO `Menus` (`Id`, `ParentId`, `Name`, `Path`, `Component`, `Title`, `Icon`, `Sort`, `Roles`, `IsSystem`, `IsHide`, `KeepAlive`, `IsIframe`, `IsHideTab`, `CreatedAt`)
VALUES (21, 20, 'DesktopDevices', 'devices', '/desktop-manage/devices', '远程桌面', NULL, 1, 'R_SUPER,R_ADMIN', 1, 0, 1, 0, 0, NOW());
-- 为超级管理员角色分配新菜单权限
INSERT INTO `RoleMenus` (`RoleId`, `MenuId`)
SELECT r.Id, 20 FROM `Roles` r WHERE r.RoleCode = 'R_SUPER';
INSERT INTO `RoleMenus` (`RoleId`, `MenuId`)
SELECT r.Id, 21 FROM `Roles` r WHERE r.RoleCode = 'R_SUPER';
-- 为管理员角色分配新菜单权限
INSERT INTO `RoleMenus` (`RoleId`, `MenuId`)
SELECT r.Id, 20 FROM `Roles` r WHERE r.RoleCode = 'R_ADMIN';
INSERT INTO `RoleMenus` (`RoleId`, `MenuId`)
SELECT r.Id, 21 FROM `Roles` r WHERE r.RoleCode = 'R_ADMIN';