feat: 修复远程桌面API路由、添加桌面管理菜单、设备Windows凭据功能
This commit is contained in:
parent
5382685f21
commit
c546d4635a
@ -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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: '确定要开机吗?' },
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -1,44 +1,69 @@
|
|||||||
<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">
|
||||||
<ElRow :gutter="20">
|
<!-- 网络扫描 -->
|
||||||
<ElCol :span="8">
|
<ElTabPane label="网络扫描" name="scan">
|
||||||
<ElFormItem label="网段地址" prop="networkSegment">
|
<ElForm :model="scanForm" :rules="scanRules" ref="scanFormRef" label-width="120px">
|
||||||
<ElInput v-model="form.networkSegment" placeholder="例如: 192.168.1.0" />
|
<ElRow :gutter="20">
|
||||||
|
<ElCol :span="8">
|
||||||
|
<ElFormItem label="网段地址" prop="networkSegment">
|
||||||
|
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="8">
|
||||||
|
<ElFormItem label="子网掩码" prop="subnetMask">
|
||||||
|
<ElInput v-model="scanForm.subnetMask" placeholder="例如: 255.255.255.0 或 /24" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="8">
|
||||||
|
<ElFormItem>
|
||||||
|
<ElButton type="primary" @click="handleStartScan" :loading="scanning">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
开始扫描
|
||||||
|
</ElButton>
|
||||||
|
<ElButton @click="handleResetScan">重置</ElButton>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<ElAlert
|
||||||
|
title="提示"
|
||||||
|
type="info"
|
||||||
|
description="扫描将检测指定网段内所有支持 Intel AMT 的设备,请确保网络连接正常。"
|
||||||
|
:closable="false"
|
||||||
|
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>
|
||||||
</ElCol>
|
<ElFormItem label="主机名">
|
||||||
<ElCol :span="8">
|
<ElInput v-model="manualForm.hostname" placeholder="可选,设备主机名" />
|
||||||
<ElFormItem label="子网掩码" prop="subnetMask">
|
</ElFormItem>
|
||||||
<ElInput v-model="form.subnetMask" placeholder="例如: 255.255.255.0 或 /24" />
|
<ElFormItem label="备注">
|
||||||
|
<ElInput v-model="manualForm.description" type="textarea" :rows="2" placeholder="可选,设备备注信息" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
|
||||||
<ElCol :span="8">
|
|
||||||
<ElFormItem>
|
<ElFormItem>
|
||||||
<ElButton type="primary" @click="handleStartScan" :loading="scanning">
|
<ElButton type="primary" @click="handleAddDevice" :loading="adding">添加设备</ElButton>
|
||||||
<el-icon><Search /></el-icon>
|
<ElButton @click="handleResetManual">重置</ElButton>
|
||||||
开始扫描
|
|
||||||
</ElButton>
|
|
||||||
<ElButton @click="handleReset">重置</ElButton>
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElForm>
|
||||||
</ElRow>
|
</ElTabPane>
|
||||||
</ElForm>
|
</ElTabs>
|
||||||
|
|
||||||
<ElAlert
|
|
||||||
title="提示"
|
|
||||||
type="info"
|
|
||||||
description="扫描将检测指定网段内所有支持 Intel AMT 的设备,请确保网络连接正常。扫描过程中请勿关闭页面。"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
/>
|
|
||||||
</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 {
|
||||||
|
|||||||
256
adminSystem/src/views/desktop-manage/devices.vue
Normal file
256
adminSystem/src/views/desktop-manage/devices.vue
Normal 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>
|
||||||
@ -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, "删除成功"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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,28 +70,75 @@ 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
|
||||||
{
|
{
|
||||||
public string NetworkSegment { get; set; } = string.Empty;
|
public string NetworkSegment { get; set; } = string.Empty;
|
||||||
|
|||||||
@ -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, "设置成功"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
656
backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.Designer.cs
generated
Normal file
656
backend-csharp/AmtScanner.Api/Migrations/20260120122638_AddDeviceWindowsCredentials.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 =>
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
25
backend-csharp/AmtScanner.Api/add_desktop_manage_menu.sql
Normal file
25
backend-csharp/AmtScanner.Api/add_desktop_manage_menu.sql
Normal 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';
|
||||||
Loading…
x
Reference in New Issue
Block a user