diff --git a/backend/app/api/v1/enhanced_scan.py b/backend/app/api/v1/enhanced_scan.py
index f596692..59307f2 100644
--- a/backend/app/api/v1/enhanced_scan.py
+++ b/backend/app/api/v1/enhanced_scan.py
@@ -156,6 +156,82 @@ def netbios_lookup(ip_address: str, timeout: int = 3):
"hostname": hostname or "Unknown"
}
+@router.post("/batch/vendor", summary="批量补全厂商信息")
+def batch_fill_vendor(
+ network_id: Optional[int] = None,
+ db: Session = Depends(get_db)
+):
+ """
+ 对已有 MAC 地址但缺少厂商信息的 IP 批量补全厂商(使用 IEEE OUI 数据库)
+ """
+ from app.models.network import IPAddress as IPAddressModel
+
+ query = db.query(IPAddressModel).filter(
+ IPAddressModel.mac_address.isnot(None),
+ IPAddressModel.vendor.is_(None)
+ )
+ if network_id:
+ query = query.filter(IPAddressModel.network_id == network_id)
+
+ items = query.all()
+ updated_count = 0
+
+ for ip in items:
+ vendor = EnhancedScanService.get_mac_vendor(ip.mac_address)
+ if vendor:
+ ip.vendor = vendor
+ updated_count += 1
+
+ if updated_count > 0:
+ db.commit()
+
+ return {
+ "message": f"已补全 {updated_count} 条厂商信息",
+ "checked": len(items),
+ "updated": updated_count
+ }
+
+@router.post("/batch/hostname", summary="批量发现主机名")
+def batch_discover_hostname(
+ network_id: Optional[int] = None,
+ enable_dns: bool = True,
+ enable_netbios: bool = True,
+ db: Session = Depends(get_db)
+):
+ """
+ 对已有 MAC 地址但缺少主机名的 IP 批量执行主机名发现(DNS PTR + NetBIOS)
+ """
+ from app.models.network import IPAddress as IPAddressModel
+
+ query = db.query(IPAddressModel).filter(
+ IPAddressModel.mac_address.isnot(None),
+ IPAddressModel.hostname.is_(None)
+ )
+ if network_id:
+ query = query.filter(IPAddressModel.network_id == network_id)
+
+ items = query.all()
+ updated_count = 0
+
+ for ip in items:
+ hostname = None
+ if enable_dns:
+ hostname = EnhancedScanService.reverse_dns_lookup(ip.ip_address, timeout=2)
+ if not hostname and enable_netbios:
+ hostname = EnhancedScanService.netbios_lookup(ip.ip_address, timeout=3)
+ if hostname:
+ ip.hostname = hostname
+ updated_count += 1
+
+ if updated_count > 0:
+ db.commit()
+
+ return {
+ "message": f"已发现 {updated_count} 条主机名",
+ "checked": len(items),
+ "updated": updated_count
+ }
+
@router.get("/online/ips", summary="获取所有在线IP列表")
def get_online_ips(
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 97c4944..6ecd2fc 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -63,20 +63,29 @@ export const scanApi = {
return api.post(`/scan/ping/${id}`)
},
// 综合扫描网段
- // options: { enable_ping, enable_arp, enable_dns, timeout }
+ // options: { enable_ping, enable_arp, enable_dns, enable_netbios, timeout }
// 单次调用覆盖全局 30s 超时,因为 /24 网段扫描要数分钟
comprehensiveScan(id, options = {}) {
const {
enable_ping = true,
enable_arp = true,
enable_dns = true,
+ enable_netbios = true,
timeout = 2
} = options
return api.post(`/scan/comprehensive/${id}`, {}, {
- params: { enable_ping, enable_arp, enable_dns, timeout },
+ params: { enable_ping, enable_arp, enable_dns, enable_netbios, timeout },
timeout: 600000 // 10 分钟,足以应付 /16 级别网段
})
},
+ // 批量补全厂商信息
+ batchFillVendor(params) {
+ return api.post('/scan/batch/vendor', {}, { params })
+ },
+ // 批量发现主机名
+ batchDiscoverHostname(params) {
+ return api.post('/scan/batch/hostname', {}, { params })
+ },
// 单个IP扫描
scanIP(ipAddress) {
return api.post(`/scan/comprehensive/ip/${ipAddress}`)
diff --git a/frontend/src/views/IPs.vue b/frontend/src/views/IPs.vue
index 38f3a58..c6460dd 100644
--- a/frontend/src/views/IPs.vue
+++ b/frontend/src/views/IPs.vue
@@ -8,6 +8,10 @@
批量扫描
+
+
+ 补全厂商
+
@@ -176,6 +180,7 @@ const route = useRoute()
const loading = ref(false)
const submitting = ref(false)
const scanning = ref(false)
+const vendorLoading = ref(false)
const dialogVisible = ref(false)
const tableData = ref([])
@@ -278,6 +283,24 @@ const startBatchScan = async () => {
}
}
+const batchFillVendor = async () => {
+ if (!filters.networkId) {
+ ElMessage.warning('请先选择网段')
+ return
+ }
+ vendorLoading.value = true
+ try {
+ const res = await scanApi.batchFillVendor({ network_id: filters.networkId })
+ ElMessage.success(res.message)
+ loadIPs()
+ } catch (error) {
+ ElMessage.error('补全厂商失败')
+ console.error('补全厂商错误:', error)
+ } finally {
+ vendorLoading.value = false
+ }
+}
+
const editIP = (row) => {
ipForm.id = row.id
ipForm.ip_address = row.ip_address