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) {
|
||||
return request.del({
|
||||
@ -62,6 +80,22 @@ export const deviceApi = {
|
||||
return request.get({
|
||||
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 }) {
|
||||
return request.post({
|
||||
url: `/api/remotedesktop/connect/${deviceId}`,
|
||||
url: `/api/remote-desktop/connect/${deviceId}`,
|
||||
params: credentials
|
||||
})
|
||||
},
|
||||
@ -229,7 +263,7 @@ export const remoteDesktopApi = {
|
||||
// 生成访问 Token
|
||||
generateToken(deviceId: number, options: { credentialId?: number; expiresInMinutes?: number; maxUseCount?: number; note?: string } = {}) {
|
||||
return request.post({
|
||||
url: `/api/remotedesktop/generate-token/${deviceId}`,
|
||||
url: `/api/remote-desktop/generate-token/${deviceId}`,
|
||||
params: options
|
||||
})
|
||||
},
|
||||
@ -237,28 +271,28 @@ export const remoteDesktopApi = {
|
||||
// 通过 Token 连接
|
||||
connectByToken(token: string) {
|
||||
return request.get({
|
||||
url: `/api/remotedesktop/connect-by-token/${token}`
|
||||
url: `/api/remote-desktop/connect-by-token/${token}`
|
||||
})
|
||||
},
|
||||
|
||||
// 验证 Token
|
||||
validateToken(token: string) {
|
||||
return request.get({
|
||||
url: `/api/remotedesktop/validate-token/${token}`
|
||||
url: `/api/remote-desktop/validate-token/${token}`
|
||||
})
|
||||
},
|
||||
|
||||
// 获取设备的所有 Token
|
||||
getDeviceTokens(deviceId: number) {
|
||||
return request.get<any[]>({
|
||||
url: `/api/remotedesktop/list-tokens/${deviceId}`
|
||||
url: `/api/remote-desktop/list-tokens/${deviceId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 撤销 Token
|
||||
revokeToken(tokenId: number) {
|
||||
return request.del({
|
||||
url: `/api/remotedesktop/revoke-token/${tokenId}`,
|
||||
url: `/api/remote-desktop/revoke-token/${tokenId}`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
},
|
||||
@ -266,14 +300,14 @@ export const remoteDesktopApi = {
|
||||
// 清理过期 Token
|
||||
cleanupTokens() {
|
||||
return request.post({
|
||||
url: '/api/remotedesktop/cleanup-tokens'
|
||||
url: '/api/remote-desktop/cleanup-tokens'
|
||||
})
|
||||
},
|
||||
|
||||
// 测试 Guacamole 连接
|
||||
test() {
|
||||
return request.get({
|
||||
url: '/api/remotedesktop/test'
|
||||
url: '/api/remote-desktop/test'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>AMT 设备列表</span>
|
||||
<span>AMT 设备管理</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>
|
||||
@ -54,17 +54,26 @@
|
||||
</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="hostname" label="主机名" min-width="120" />
|
||||
<ElTableColumn label="发现时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.discoveredAt) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="380" fixed="right">
|
||||
<ElTableColumn label="操作" width="420" fixed="right">
|
||||
<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 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>
|
||||
@ -95,6 +104,25 @@
|
||||
|
||||
<!-- 远程桌面弹窗 -->
|
||||
<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>
|
||||
|
||||
@ -114,8 +142,11 @@ const isCheckingStatus = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const showHardwareModal = ref(false)
|
||||
const showRemoteDesktopModal = ref(false)
|
||||
const showCredentialsDialog = ref(false)
|
||||
const selectedDeviceId = ref(0)
|
||||
const selectedDevice = ref<any>(null)
|
||||
const credentialsForm = ref({ username: '', password: '' })
|
||||
const savingCredentials = ref(false)
|
||||
|
||||
let statusCheckInterval: number | null = null
|
||||
|
||||
@ -223,10 +254,43 @@ const handleRemoteDesktop = (device: any) => {
|
||||
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: '确定要开机吗?' },
|
||||
|
||||
@ -29,8 +29,11 @@
|
||||
<ElTabPane label="快速连接" name="quick">
|
||||
<div class="quick-connect">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
使用默认 Windows 凭据快速连接,无需输入密码
|
||||
使用设备配置的 Windows 账号快速连接
|
||||
</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>
|
||||
@ -40,13 +43,6 @@
|
||||
<ElTabPane label="生成分享链接" name="share">
|
||||
<div class="share-form">
|
||||
<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="有效期(分钟)">
|
||||
<ElInputNumber v-model="tokenForm.expiresInMinutes" :min="5" :max="1440" />
|
||||
</ElFormItem>
|
||||
@ -73,22 +69,6 @@
|
||||
</div>
|
||||
</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">
|
||||
<div class="tokens-list">
|
||||
<ElTable :data="deviceTokens" v-loading="loadingTokens" size="small">
|
||||
@ -124,7 +104,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { windowsCredentialsApi, remoteDesktopApi } from '@/api/amt'
|
||||
import { remoteDesktopApi } from '@/api/amt'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@ -147,21 +127,11 @@ const connecting = ref(false)
|
||||
const generating = ref(false)
|
||||
const isFullscreen = ref(false)
|
||||
const rdpFrame = ref<HTMLIFrameElement | null>(null)
|
||||
const credentials = ref<any[]>([])
|
||||
const manualCredentials = ref({ username: '', password: '' })
|
||||
const tokenForm = ref({ credentialId: null as number | null, expiresInMinutes: 30, maxUseCount: 1, note: '' })
|
||||
const tokenForm = ref({ expiresInMinutes: 30, maxUseCount: 1, note: '' })
|
||||
const generatedToken = ref<any>(null)
|
||||
const deviceTokens = ref<any[]>([])
|
||||
const loadingTokens = ref(false)
|
||||
|
||||
const loadCredentials = async () => {
|
||||
try {
|
||||
credentials.value = await windowsCredentialsApi.getAll()
|
||||
} catch (error) {
|
||||
console.error('加载凭据失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadDeviceTokens = async () => {
|
||||
if (!props.device?.id) return
|
||||
loadingTokens.value = true
|
||||
@ -190,28 +160,7 @@ const quickConnect = async () => {
|
||||
ElMessage.error(connectResponse.error || '连接失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
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 || '连接远程桌面服务失败')
|
||||
ElMessage.error(error.message || '连接失败,请先配置设备的 Windows 登录账号')
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
@ -221,13 +170,11 @@ const generateToken = async () => {
|
||||
generating.value = true
|
||||
try {
|
||||
const response = await remoteDesktopApi.generateToken(props.device.id, {
|
||||
credentialId: tokenForm.value.credentialId || undefined,
|
||||
expiresInMinutes: tokenForm.value.expiresInMinutes,
|
||||
maxUseCount: tokenForm.value.maxUseCount,
|
||||
note: tokenForm.value.note || undefined
|
||||
})
|
||||
if (response.success) {
|
||||
// 修正 accessUrl 为前端 Hash 路由格式
|
||||
response.accessUrl = `${window.location.origin}/#/remote/${response.token}`
|
||||
generatedToken.value = response
|
||||
ElMessage.success('链接已生成')
|
||||
@ -250,7 +197,6 @@ const copyLink = async () => {
|
||||
}
|
||||
|
||||
const copyTokenLink = async (token: any) => {
|
||||
// 使用 Hash 路由格式
|
||||
const url = `${window.location.origin}/#/remote/${token.token}`
|
||||
await navigator.clipboard.writeText(url)
|
||||
ElMessage.success('链接已复制')
|
||||
@ -279,10 +225,8 @@ const handleClose = () => { connectionUrl.value = ''; visible.value = false }
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal) {
|
||||
connectionUrl.value = ''
|
||||
manualCredentials.value = { username: '', password: '' }
|
||||
generatedToken.value = null
|
||||
activeTab.value = 'quick'
|
||||
loadCredentials()
|
||||
loadDeviceTokens()
|
||||
}
|
||||
})
|
||||
@ -293,7 +237,6 @@ watch(() => props.modelValue, (newVal) => {
|
||||
.header-actions { margin-right: 40px; display: flex; align-items: center; }
|
||||
.connection-options { padding: 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; }
|
||||
.generated-link { margin-top: 20px; }
|
||||
.link-box { margin-top: 10px; }
|
||||
|
||||
@ -1,44 +1,69 @@
|
||||
<template>
|
||||
<div class="scan-page">
|
||||
<!-- 扫描配置 -->
|
||||
<ElCard class="scan-config" shadow="never">
|
||||
<!-- 添加方式选择 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>网络扫描配置</span>
|
||||
<span>设备添加</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElForm :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="网段地址" prop="networkSegment">
|
||||
<ElInput v-model="form.networkSegment" placeholder="例如: 192.168.1.0" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="子网掩码" prop="subnetMask">
|
||||
<ElInput v-model="form.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="handleReset">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
<ElTabs v-model="activeTab">
|
||||
<!-- 网络扫描 -->
|
||||
<ElTabPane label="网络扫描" name="scan">
|
||||
<ElForm :model="scanForm" :rules="scanRules" ref="scanFormRef" label-width="120px">
|
||||
<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
|
||||
/>
|
||||
<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 label="主机名">
|
||||
<ElInput v-model="manualForm.hostname" placeholder="可选,设备主机名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput v-model="manualForm.description" type="textarea" :rows="2" placeholder="可选,设备备注信息" />
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleAddDevice" :loading="adding">添加设备</ElButton>
|
||||
<ElButton @click="handleResetManual">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
|
||||
<!-- 扫描进度 -->
|
||||
@ -83,19 +108,30 @@ import { ref, reactive, computed, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { scanApi } from '@/api/amt'
|
||||
import { scanApi, deviceApi } from '@/api/amt'
|
||||
|
||||
defineOptions({ name: 'AmtScan' })
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref()
|
||||
const activeTab = ref('scan')
|
||||
const scanFormRef = ref()
|
||||
const manualFormRef = ref()
|
||||
const scanning = ref(false)
|
||||
const adding = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
// 扫描表单
|
||||
const scanForm = reactive({
|
||||
networkSegment: '192.168.1.0',
|
||||
subnetMask: '255.255.255.0'
|
||||
})
|
||||
|
||||
// 手动添加表单
|
||||
const manualForm = reactive({
|
||||
ipAddress: '',
|
||||
hostname: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const scanProgress = reactive({
|
||||
taskId: '',
|
||||
scannedCount: 0,
|
||||
@ -114,7 +150,7 @@ const progressStatus = computed(() => {
|
||||
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]?)$/
|
||||
if (!value) {
|
||||
callback(new Error('请输入网段地址'))
|
||||
callback(new Error('请输入 IP 地址'))
|
||||
} else if (!ipRegex.test(value)) {
|
||||
callback(new Error('请输入有效的 IP 地址'))
|
||||
} else {
|
||||
@ -142,17 +178,22 @@ const validateSubnetMask = (_rule: any, value: string, callback: Function) => {
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
const scanRules = {
|
||||
networkSegment: [{ validator: validateIp, trigger: 'blur' }],
|
||||
subnetMask: [{ validator: validateSubnetMask, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const manualRules = {
|
||||
ipAddress: [{ validator: validateIp, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
let pollTimer: number | null = null
|
||||
|
||||
// 扫描相关方法
|
||||
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) {
|
||||
scanning.value = true
|
||||
scanProgress.status = 'running'
|
||||
@ -162,11 +203,9 @@ const handleStartScan = async () => {
|
||||
scanProgress.progressPercentage = 0
|
||||
|
||||
try {
|
||||
const result = await scanApi.startScan(form.networkSegment, form.subnetMask)
|
||||
const result = await scanApi.startScan(scanForm.networkSegment, scanForm.subnetMask)
|
||||
scanProgress.taskId = result.taskId
|
||||
ElMessage.success('扫描任务已启动')
|
||||
|
||||
// 轮询获取进度
|
||||
startPolling()
|
||||
} catch (error) {
|
||||
scanning.value = false
|
||||
@ -220,15 +259,46 @@ const handleCancelScan = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formRef.value?.resetFields()
|
||||
const handleResetScan = () => {
|
||||
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 = () => {
|
||||
router.push('/amt/devices')
|
||||
}
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
@ -239,10 +309,6 @@ onUnmounted(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.scan-config {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -252,7 +318,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.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]
|
||||
public async Task<ActionResult<List<CredentialResponse>>> GetAllCredentials()
|
||||
public async Task<ActionResult<ApiResponse<List<CredentialResponse>>>> GetAllCredentials()
|
||||
{
|
||||
var credentials = await _credentialService.GetAllCredentialsAsync();
|
||||
return credentials.Select(c => new CredentialResponse
|
||||
var result = credentials.Select(c => new CredentialResponse
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.Name,
|
||||
@ -28,10 +28,11 @@ public class CredentialsController : ControllerBase
|
||||
Description = c.Description,
|
||||
HasPassword = !string.IsNullOrEmpty(c.Password)
|
||||
}).ToList();
|
||||
return Ok(ApiResponse<List<CredentialResponse>>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<CredentialResponse>> CreateCredential([FromBody] CredentialRequest request)
|
||||
public async Task<ActionResult<ApiResponse<CredentialResponse>>> CreateCredential([FromBody] CredentialRequest request)
|
||||
{
|
||||
var credential = new AmtCredential
|
||||
{
|
||||
@ -44,7 +45,7 @@ public class CredentialsController : ControllerBase
|
||||
|
||||
var created = await _credentialService.CreateCredentialAsync(credential);
|
||||
|
||||
return CreatedAtAction(nameof(GetAllCredentials), new { id = created.Id }, new CredentialResponse
|
||||
var response = new CredentialResponse
|
||||
{
|
||||
Id = created.Id,
|
||||
Name = created.Name,
|
||||
@ -52,11 +53,12 @@ public class CredentialsController : ControllerBase
|
||||
IsDefault = created.IsDefault,
|
||||
Description = created.Description,
|
||||
HasPassword = true
|
||||
});
|
||||
};
|
||||
return Ok(ApiResponse<CredentialResponse>.Success(response, "创建成功"));
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
@ -71,7 +73,7 @@ public class CredentialsController : ControllerBase
|
||||
|
||||
var updated = await _credentialService.UpdateCredentialAsync(id, credential);
|
||||
|
||||
return new CredentialResponse
|
||||
var response = new CredentialResponse
|
||||
{
|
||||
Id = updated.Id,
|
||||
Name = updated.Name,
|
||||
@ -80,18 +82,19 @@ public class CredentialsController : ControllerBase
|
||||
Description = updated.Description,
|
||||
HasPassword = !string.IsNullOrEmpty(updated.Password)
|
||||
};
|
||||
return Ok(ApiResponse<CredentialResponse>.Success(response, "更新成功"));
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
return Ok(ApiResponse<CredentialResponse>.Fail(404, "凭据不存在"));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteCredential(long id)
|
||||
public async Task<ActionResult<ApiResponse<object>>> DeleteCredential(long id)
|
||||
{
|
||||
await _credentialService.DeleteCredentialAsync(id);
|
||||
return NoContent();
|
||||
return Ok(ApiResponse<object>.Success(null, "删除成功"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,45 +29,166 @@ public class DevicesController : ControllerBase
|
||||
}
|
||||
|
||||
[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}")]
|
||||
public async Task<ActionResult<AmtDevice>> GetDevice(long id)
|
||||
public async Task<ActionResult<ApiResponse<AmtDevice>>> GetDevice(long id)
|
||||
{
|
||||
var device = await _context.AmtDevices.FindAsync(id);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
return NotFound();
|
||||
return Ok(ApiResponse<AmtDevice>.Fail(404, "设备不存在"));
|
||||
}
|
||||
|
||||
return device;
|
||||
return Ok(ApiResponse<AmtDevice>.Success(device));
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
return NotFound();
|
||||
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
|
||||
}
|
||||
|
||||
_context.AmtDevices.Remove(device);
|
||||
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>
|
||||
[HttpGet("status")]
|
||||
public async Task<ActionResult<List<DeviceStatusDto>>> CheckAllDevicesStatus()
|
||||
public async Task<ActionResult<ApiResponse<List<DeviceStatusDto>>>> CheckAllDevicesStatus()
|
||||
{
|
||||
var devices = await _context.AmtDevices.ToListAsync();
|
||||
var credentials = await _context.AmtCredentials.ToListAsync();
|
||||
@ -107,20 +228,20 @@ public class DevicesController : ControllerBase
|
||||
// 保存更新
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return statusList;
|
||||
return Ok(ApiResponse<List<DeviceStatusDto>>.Success(statusList));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测单个设备的在线状态
|
||||
/// </summary>
|
||||
[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);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
return NotFound();
|
||||
return Ok(ApiResponse<DeviceStatusDto>.Fail(404, "设备不存在"));
|
||||
}
|
||||
|
||||
var credentials = await _context.AmtCredentials.ToListAsync();
|
||||
@ -142,13 +263,13 @@ public class DevicesController : ControllerBase
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return new DeviceStatusDto
|
||||
return Ok(ApiResponse<DeviceStatusDto>.Success(new DeviceStatusDto
|
||||
{
|
||||
Id = device.Id,
|
||||
IpAddress = device.IpAddress,
|
||||
AmtOnline = amtOnline,
|
||||
OsOnline = osOnline
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -246,3 +367,43 @@ public class DeviceStatusDto
|
||||
public bool AmtOnline { 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}")]
|
||||
public async Task<ActionResult<HardwareInfoDto>> GetHardwareInfo(
|
||||
public async Task<ActionResult<ApiResponse<HardwareInfoDto>>> GetHardwareInfo(
|
||||
long deviceId,
|
||||
[FromQuery] bool refresh = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _hardwareInfoService.GetHardwareInfoAsync(deviceId, refresh);
|
||||
return Ok(result);
|
||||
return Ok(ApiResponse<HardwareInfoDto>.Success(result));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_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")]
|
||||
public async Task<ActionResult<List<BatchHardwareInfoResult>>> GetBatchHardwareInfo(
|
||||
public async Task<ActionResult<ApiResponse<BatchHardwareInfoResponse>>> GetBatchHardwareInfo(
|
||||
[FromBody] BatchHardwareInfoRequest request)
|
||||
{
|
||||
try
|
||||
@ -45,12 +45,17 @@ public class HardwareInfoController : ControllerBase
|
||||
var results = await _hardwareInfoService.GetBatchHardwareInfoAsync(
|
||||
request.DeviceIds,
|
||||
request.Refresh);
|
||||
return Ok(new { results });
|
||||
return Ok(ApiResponse<BatchHardwareInfoResponse>.Success(new BatchHardwareInfoResponse { Results = results }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_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.Models;
|
||||
using AmtScanner.Api.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -30,18 +31,18 @@ public class PowerController : ControllerBase
|
||||
/// 获取设备电源状态
|
||||
/// </summary>
|
||||
[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);
|
||||
if (device == null)
|
||||
{
|
||||
return NotFound(new { error = "设备不存在" });
|
||||
return Ok(ApiResponse<PowerStateResponse>.Fail(404, "设备不存在"));
|
||||
}
|
||||
|
||||
var credential = await _credentialService.GetDefaultCredentialAsync();
|
||||
if (credential == null)
|
||||
{
|
||||
return BadRequest(new { error = "没有配置默认凭据" });
|
||||
return Ok(ApiResponse<PowerStateResponse>.Fail(400, "没有配置默认凭据"));
|
||||
}
|
||||
|
||||
var openPorts = new List<int>();
|
||||
@ -53,13 +54,13 @@ public class PowerController : ControllerBase
|
||||
|
||||
if (openPorts.Count == 0)
|
||||
{
|
||||
return Ok(new PowerStateResponse
|
||||
return Ok(ApiResponse<PowerStateResponse>.Success(new PowerStateResponse
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
IpAddress = device.IpAddress,
|
||||
Success = false,
|
||||
Error = "设备离线或AMT端口不可用"
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// 解密密码
|
||||
@ -71,7 +72,7 @@ public class PowerController : ControllerBase
|
||||
decryptedPassword,
|
||||
openPorts);
|
||||
|
||||
return Ok(new PowerStateResponse
|
||||
return Ok(ApiResponse<PowerStateResponse>.Success(new PowerStateResponse
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
IpAddress = device.IpAddress,
|
||||
@ -79,14 +80,14 @@ public class PowerController : ControllerBase
|
||||
PowerState = result.PowerState,
|
||||
PowerStateText = result.PowerStateText,
|
||||
Error = result.Error
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开机
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
@ -95,7 +96,7 @@ public class PowerController : ControllerBase
|
||||
/// 关机(优雅关机)
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
@ -104,7 +105,7 @@ public class PowerController : ControllerBase
|
||||
/// 强制关机
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
@ -113,7 +114,7 @@ public class PowerController : ControllerBase
|
||||
/// 重启(优雅重启)
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
@ -122,7 +123,7 @@ public class PowerController : ControllerBase
|
||||
/// 强制重启
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
@ -131,23 +132,23 @@ public class PowerController : ControllerBase
|
||||
/// 电源循环(硬重启)
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (device == null)
|
||||
{
|
||||
return NotFound(new { error = "设备不存在" });
|
||||
return Ok(ApiResponse<PowerActionResponse>.Fail(404, "设备不存在"));
|
||||
}
|
||||
|
||||
var credential = await _credentialService.GetDefaultCredentialAsync();
|
||||
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)
|
||||
{
|
||||
return Ok(new PowerActionResponse
|
||||
return Ok(ApiResponse<PowerActionResponse>.Success(new PowerActionResponse
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
IpAddress = device.IpAddress,
|
||||
Action = action.ToString(),
|
||||
Success = false,
|
||||
Error = "AMT端口不可用"
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// 解密密码
|
||||
@ -175,7 +176,7 @@ public class PowerController : ControllerBase
|
||||
openPorts,
|
||||
action);
|
||||
|
||||
return Ok(new PowerActionResponse
|
||||
return Ok(ApiResponse<PowerActionResponse>.Success(new PowerActionResponse
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
IpAddress = device.IpAddress,
|
||||
@ -183,7 +184,7 @@ public class PowerController : ControllerBase
|
||||
Success = result.Success,
|
||||
Message = result.Message,
|
||||
Error = result.Error
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
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.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -8,7 +8,7 @@ using System.Security.Cryptography;
|
||||
namespace AmtScanner.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/remote-desktop")]
|
||||
public class RemoteDesktopController : ControllerBase
|
||||
{
|
||||
private readonly IGuacamoleService _guacamoleService;
|
||||
@ -25,6 +25,15 @@ public class RemoteDesktopController : ControllerBase
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 健康检查
|
||||
/// </summary>
|
||||
[HttpGet("health")]
|
||||
public IActionResult Health()
|
||||
{
|
||||
return Ok(ApiResponse<object>.Success(new { status = "ok" }, "远程桌面服务正常"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成远程访问 Token(管理员使用)
|
||||
/// </summary>
|
||||
@ -37,18 +46,9 @@ public class RemoteDesktopController : ControllerBase
|
||||
if (device == null)
|
||||
return Ok(ApiResponse<GenerateTokenResponse>.Fail(404, "设备不存在"));
|
||||
|
||||
WindowsCredential? credential = null;
|
||||
if (request.CredentialId.HasValue)
|
||||
{
|
||||
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 凭据"));
|
||||
// 检查设备是否配置了 Windows 凭据
|
||||
if (string.IsNullOrEmpty(device.WindowsUsername) || string.IsNullOrEmpty(device.WindowsPassword))
|
||||
return Ok(ApiResponse<GenerateTokenResponse>.Fail(400, "请先为该设备配置 Windows 登录凭据"));
|
||||
|
||||
var token = GenerateRandomToken();
|
||||
var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30);
|
||||
@ -57,7 +57,6 @@ public class RemoteDesktopController : ControllerBase
|
||||
{
|
||||
Token = token,
|
||||
DeviceId = deviceId,
|
||||
WindowsCredentialId = credential.Id,
|
||||
ExpiresAt = expiresAt,
|
||||
MaxUseCount = request.MaxUseCount ?? 1,
|
||||
Note = request.Note
|
||||
@ -67,7 +66,7 @@ public class RemoteDesktopController : ControllerBase
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
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}",
|
||||
device.IpAddress, expiresAt);
|
||||
@ -91,7 +90,6 @@ public class RemoteDesktopController : ControllerBase
|
||||
{
|
||||
var accessToken = await _context.RemoteAccessTokens
|
||||
.Include(t => t.Device)
|
||||
.Include(t => t.WindowsCredential)
|
||||
.FirstOrDefaultAsync(t => t.Token == token);
|
||||
|
||||
if (accessToken == null)
|
||||
@ -100,8 +98,13 @@ public class RemoteDesktopController : ControllerBase
|
||||
if (!accessToken.IsValid())
|
||||
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "访问链接已过期或已达到使用次数上限"));
|
||||
|
||||
if (accessToken.Device == null || accessToken.WindowsCredential == null)
|
||||
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(400, "设备或凭据信息不完整"));
|
||||
if (accessToken.Device == null)
|
||||
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.UsedAt = DateTime.UtcNow;
|
||||
@ -114,7 +117,7 @@ public class RemoteDesktopController : ControllerBase
|
||||
var connectionName = $"AMT-{accessToken.Device.IpAddress}";
|
||||
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(
|
||||
guacToken, connectionName, accessToken.Device.IpAddress,
|
||||
accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password);
|
||||
accessToken.Device.WindowsUsername!, password);
|
||||
|
||||
if (string.IsNullOrEmpty(connectionId))
|
||||
return Ok(ApiResponse<RemoteDesktopResponse>.Fail(500, "创建远程连接失败"));
|
||||
@ -131,6 +134,7 @@ public class RemoteDesktopController : ControllerBase
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 验证 Token 是否有效
|
||||
/// </summary>
|
||||
@ -204,9 +208,10 @@ public class RemoteDesktopController : ControllerBase
|
||||
var count = await _context.RemoteAccessTokens
|
||||
.Where(t => t.ExpiresAt < DateTime.UtcNow)
|
||||
.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>
|
||||
@ -264,7 +269,6 @@ public class RemoteDesktopController : ControllerBase
|
||||
|
||||
public class GenerateTokenRequest
|
||||
{
|
||||
public long? CredentialId { get; set; }
|
||||
public int? ExpiresInMinutes { get; set; } = 30;
|
||||
public int? MaxUseCount { get; set; } = 1;
|
||||
public string? Note { get; set; }
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using AmtScanner.Api.Models;
|
||||
using AmtScanner.Api.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace AmtScanner.Api.Controllers;
|
||||
|
||||
@ -12,6 +14,9 @@ public class ScanController : ControllerBase
|
||||
private readonly IHubContext<ScanProgressHub> _hubContext;
|
||||
private readonly ILogger<ScanController> _logger;
|
||||
|
||||
// 存储扫描进度状态
|
||||
private static readonly ConcurrentDictionary<string, ScanStatusInfo> _scanStatuses = new();
|
||||
|
||||
public ScanController(
|
||||
IAmtScannerService scannerService,
|
||||
IHubContext<ScanProgressHub> hubContext,
|
||||
@ -23,17 +28,36 @@ public class ScanController : ControllerBase
|
||||
}
|
||||
|
||||
[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();
|
||||
|
||||
_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
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
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);
|
||||
});
|
||||
|
||||
@ -46,28 +70,75 @@ public class ScanController : ControllerBase
|
||||
progress
|
||||
);
|
||||
|
||||
// 更新状态为完成
|
||||
if (_scanStatuses.TryGetValue(taskId, out var status))
|
||||
{
|
||||
status.Status = "completed";
|
||||
}
|
||||
|
||||
// Send completion notification
|
||||
_logger.LogInformation("Scan task {TaskId} completed", taskId);
|
||||
await _hubContext.Clients.All.SendAsync("ScanCompleted", new { taskId });
|
||||
}
|
||||
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);
|
||||
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}")]
|
||||
public IActionResult CancelScan(string taskId)
|
||||
public ActionResult<ApiResponse<object>> CancelScan(string 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 string NetworkSegment { get; set; } = string.Empty;
|
||||
|
||||
@ -22,7 +22,7 @@ public class WindowsCredentialsController : ControllerBase
|
||||
/// 获取所有 Windows 凭据
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<WindowsCredentialDto>>> GetAll()
|
||||
public async Task<ActionResult<ApiResponse<List<WindowsCredentialDto>>>> GetAll()
|
||||
{
|
||||
var credentials = await _context.WindowsCredentials
|
||||
.OrderByDescending(c => c.IsDefault)
|
||||
@ -39,14 +39,14 @@ public class WindowsCredentialsController : ControllerBase
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(credentials);
|
||||
return Ok(ApiResponse<List<WindowsCredentialDto>>.Success(credentials));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Windows 凭据
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<WindowsCredentialDto>> Create([FromBody] CreateWindowsCredentialRequest request)
|
||||
public async Task<ActionResult<ApiResponse<WindowsCredentialDto>>> Create([FromBody] CreateWindowsCredentialRequest request)
|
||||
{
|
||||
// 如果设为默认,取消其他默认
|
||||
if (request.IsDefault)
|
||||
@ -71,7 +71,7 @@ public class WindowsCredentialsController : ControllerBase
|
||||
|
||||
_logger.LogInformation("Created Windows credential: {Name}", credential.Name);
|
||||
|
||||
return Ok(new WindowsCredentialDto
|
||||
var dto = new WindowsCredentialDto
|
||||
{
|
||||
Id = credential.Id,
|
||||
Name = credential.Name,
|
||||
@ -80,19 +80,21 @@ public class WindowsCredentialsController : ControllerBase
|
||||
IsDefault = credential.IsDefault,
|
||||
Note = credential.Note,
|
||||
CreatedAt = credential.CreatedAt
|
||||
});
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<WindowsCredentialDto>.Success(dto, "创建成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新 Windows 凭据
|
||||
/// </summary>
|
||||
[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);
|
||||
if (credential == null)
|
||||
{
|
||||
return NotFound(new { error = "凭据不存在" });
|
||||
return Ok(ApiResponse<object>.Fail(404, "凭据不存在"));
|
||||
}
|
||||
|
||||
// 如果设为默认,取消其他默认
|
||||
@ -116,19 +118,19 @@ public class WindowsCredentialsController : ControllerBase
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new { success = true });
|
||||
return Ok(ApiResponse<object>.Success(null, "更新成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除 Windows 凭据
|
||||
/// </summary>
|
||||
[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);
|
||||
if (credential == null)
|
||||
{
|
||||
return NotFound(new { error = "凭据不存在" });
|
||||
return Ok(ApiResponse<object>.Fail(404, "凭据不存在"));
|
||||
}
|
||||
|
||||
_context.WindowsCredentials.Remove(credential);
|
||||
@ -136,19 +138,19 @@ public class WindowsCredentialsController : ControllerBase
|
||||
|
||||
_logger.LogInformation("Deleted Windows credential: {Name}", credential.Name);
|
||||
|
||||
return Ok(new { success = true });
|
||||
return Ok(ApiResponse<object>.Success(null, "删除成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认凭据
|
||||
/// </summary>
|
||||
[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);
|
||||
if (credential == null)
|
||||
{
|
||||
return NotFound(new { error = "凭据不存在" });
|
||||
return Ok(ApiResponse<object>.Fail(404, "凭据不存在"));
|
||||
}
|
||||
|
||||
// 取消其他默认
|
||||
@ -159,7 +161,7 @@ public class WindowsCredentialsController : ControllerBase
|
||||
credential.IsDefault = true;
|
||||
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)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<RemoteAccessToken>()
|
||||
.HasOne(t => t.WindowsCredential)
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.WindowsCredentialId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// User 配置
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(u => u.UserName)
|
||||
|
||||
@ -101,18 +101,21 @@ public static class DbSeeder
|
||||
|
||||
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 = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
|
||||
|
||||
// AMT 设备管理菜单(系统内置)
|
||||
new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "设备管理", 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 = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备列表", KeepAlive = true, 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 = 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 = 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 = 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 },
|
||||
|
||||
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")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("WindowsPassword")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("WindowsUsername")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IpAddress")
|
||||
@ -303,9 +309,6 @@ namespace AmtScanner.Api.Migrations
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long?>("WindowsCredentialId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
@ -313,8 +316,6 @@ namespace AmtScanner.Api.Migrations
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("WindowsCredentialId");
|
||||
|
||||
b.ToTable("RemoteAccessTokens");
|
||||
});
|
||||
|
||||
@ -569,14 +570,7 @@ namespace AmtScanner.Api.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AmtScanner.Api.Models.WindowsCredential", "WindowsCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("WindowsCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("WindowsCredential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||
|
||||
@ -30,6 +30,16 @@ public class AmtDevice
|
||||
/// </summary>
|
||||
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 LastSeenAt { get; set; }
|
||||
|
||||
@ -27,16 +27,6 @@ public class RemoteAccessToken
|
||||
/// </summary>
|
||||
public AmtDevice? Device { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的 Windows 凭据 ID
|
||||
/// </summary>
|
||||
public long? WindowsCredentialId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的 Windows 凭据
|
||||
/// </summary>
|
||||
public WindowsCredential? WindowsCredential { get; set; }
|
||||
|
||||
/// <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