| | |
| | | [int]$ConnectTimeoutSec = 10, |
| | | [int]$CommandTimeoutSec = 15, |
| | | [int]$HttpTimeoutSec = 30, |
| | | [string]$StateFile = (Join-Path $PSScriptRoot 'j22-collector-state.json'), |
| | | [string]$LogFile = (Join-Path $PSScriptRoot 'j22-collector.log'), |
| | | # 通道号偏移:把厂商自己软件里显示的通道号报给后端,让界面与登记能跟厂商软件对上。 |
| | | # 现场核对过:甲方软件按 01–30 编号,而它库里的表名 Ws<N>_IV_0 比它小 1。 |
| | | # 依据是四组记录互证(软件里的通道号 ↔ 它的库名 ↔ 我们的表名): |
| | | # 02 ↔ DB_20260921_165803 ↔ Ws1 ;03 ↔ DB_20260927_172118 ↔ Ws2 |
| | | # 25 ↔ DB_20260927_155145 ↔ Ws24 ;28 ↔ DB_20261002_174234 ↔ Ws27 |
| | | # 它只是展示名(不参与通道身份),所以若现场发现哪个通道对不上,改这个值即可,不用改代码、不影响数据。 |
| | | [int]$ChannelNoOffset = 1, |
| | | # 下面两个留空即在正文里解析成"脚本同目录";不要写成 param 默认值: |
| | | # Windows PowerShell 的参数默认值不在脚本作用域里求值,$PSScriptRoot 在那里是空的(现场实测踩到)。 |
| | | [string]$StateFile, |
| | | [string]$LogFile, |
| | | [switch]$Once |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Continue' |
| | | |
| | | # 脚本所在目录:$PSScriptRoot 为主,取不到时退回命令路径、再退回当前目录。 |
| | | $ScriptDirectory = $PSScriptRoot |
| | | if ([string]::IsNullOrEmpty($ScriptDirectory)) { $ScriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path } |
| | | if ([string]::IsNullOrEmpty($ScriptDirectory)) { $ScriptDirectory = (Get-Location).Path } |
| | | if ([string]::IsNullOrEmpty($StateFile)) { $StateFile = Join-Path $ScriptDirectory 'j22-collector-state.json' } |
| | | if ([string]::IsNullOrEmpty($LogFile)) { $LogFile = Join-Path $ScriptDirectory 'j22-collector.log' } |
| | | |
| | | # ---------------------------------------------------------------- 日志 |
| | | function Write-Log([string]$message) { |
| | | $line = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' ' + $message |
| | | try { Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8 } catch { } |
| | | Write-Host $line |
| | | } |
| | | |
| | | # ---------------------------------------------------------------- 取值转换 |
| | | # 厂商列可能为 NULL:直接强转 [double]/[int] 会抛异常、让整个通道从此不再推进。 |
| | | # 缺列一律转成 JSON null,后端把测量列的缺失当"设备没给",不写零代替缺失。 |
| | | function To-Text($value) { |
| | | if ($null -eq $value -or $value -is [System.DBNull]) { return $null } |
| | | return [string]$value |
| | | } |
| | | |
| | | function To-Int($value) { |
| | | if ($null -eq $value -or $value -is [System.DBNull]) { return $null } |
| | | return [int]$value |
| | | } |
| | | |
| | | function To-Num($value) { |
| | | if ($null -eq $value -or $value -is [System.DBNull]) { return $null } |
| | | return [double]$value |
| | | } |
| | | |
| | | # ---------------------------------------------------------------- 读取位置 |
| | |
| | | [void]$reader.GetValues($values) |
| | | [void]$rows.Add($values) |
| | | } |
| | | return $rows |
| | | # 必须用逗号包一层返回:函数返回集合时 PowerShell 会拆一层,只有一行结果时 |
| | | # 调用方 foreach 拿到的就不是"行"而是那一行的列值本身,$row[0] 会变成字符串首字符。 |
| | | return ,$rows |
| | | } |
| | | finally { $reader.Close() } |
| | | } |
| | |
| | | having max(last_user_update) > dateadd(minute, -$LookbackMinutes, getdate()) |
| | | "@ $null |
| | | foreach ($row in $rows) { [void]$databases.Add([string]$row[0]) } |
| | | return $databases |
| | | if ($databases.Count -gt 0) { |
| | | Write-Log (" 最近 $LookbackMinutes 分钟内有写入的库:" + $databases.Count + " 个") |
| | | return $databases |
| | | } |
| | | # 没有库在写不等于没有东西可报:通道清单(拓扑)仍要让操作员看到, |
| | | # 所以照样列最近的库;它们的水位之后的记录通常为空,等于不推数据。 |
| | | Write-Log " 最近 $LookbackMinutes 分钟内没有库在写入(现在没有测试在跑?),改为列出最近的库" |
| | | } |
| | | catch { |
| | | $failure = $_.Exception.Message |
| | | Write-Log (' 活跃库探测失败,退回按建库时间取库列表: ' + $failure) |
| | | Write-Log (' 活跃库探测失败,改为列出最近的库: ' + $failure) |
| | | } |
| | | |
| | | try { |
| | |
| | | select top 60 name from sys.databases where name like 'DB[_]%' order by create_date desc |
| | | "@ $null |
| | | foreach ($row in $rows) { [void]$databases.Add([string]$row[0]) } |
| | | Write-Log (" 已列出最近的库:" + $databases.Count + " 个") |
| | | return $databases |
| | | } |
| | | catch { |
| | |
| | | function Send-Topology($channels) { |
| | | if ($channels.Count -eq 0) { return $true } |
| | | $rows = @() |
| | | foreach ($channel in $channels) { $rows += [pscustomobject]@{ channel = $channel } } |
| | | foreach ($channel in $channels) { |
| | | # channel 是身份(表名里的号);channelNo 是厂商软件里显示的号,只用于界面显示与登记 |
| | | $rows += [pscustomobject]@{ channel = $channel; channelNo = ($channel + $ChannelNoOffset) } |
| | | } |
| | | $payload = [pscustomobject]@{ kind = 'TOPOLOGY'; rows = $rows } | ConvertTo-Json -Depth 4 -Compress |
| | | return Send-Payload $payload |
| | | } |
| | |
| | | $positions = Get-ReadPositions |
| | | $seenChannels = New-Object System.Collections.ArrayList |
| | | $sentRows = 0 |
| | | $tablesSeen = 0 |
| | | $emptyChannels = 0 |
| | | |
| | | foreach ($database in (Find-ActiveDatabases $connection)) { |
| | | $databases = Find-ActiveDatabases $connection |
| | | foreach ($database in $databases) { |
| | | foreach ($table in (Find-ChannelTables $connection $database)) { |
| | | if ($table -notmatch '^Ws(\d+)_IV_0$') { continue } |
| | | $channel = [int]$Matches[1] |
| | | $tablesSeen++ |
| | | if (-not $seenChannels.Contains($channel)) { [void]$seenChannels.Add($channel) } |
| | | |
| | | $key = "$database/$table" |
| | |
| | | } |
| | | catch { Write-Log (" 读取失败 [$key]: " + $_.Exception.Message); continue } |
| | | |
| | | if ($rows.Count -eq 0) { continue } |
| | | if ($rows.Count -eq 0) { $emptyChannels++; continue } |
| | | |
| | | # 排序与截断在这里做:服务端不做排序 |
| | | $rows = @($rows | Sort-Object { [long]$_.record_time } | Select-Object -First $MaxRowsPerChannel) |
| | |
| | | } |
| | | |
| | | # 拓扑:这台机器当前有数据表的通道。端点忽略重复声明,所以每轮都发没有副作用; |
| | | # 它同时是候选"最近数据时间"的来源之外的通道清单(最近数据时间取采样行)。 |
| | | # 它同时是候选的通道清单(最近数据时间取采样行,不取上报时间)。 |
| | | [void](Send-Topology $seenChannels) |
| | | Save-ReadPositions $positions |
| | | Write-Log (" 本轮扫过 {0} 张通道表 / {1} 个通道;其中 {2} 个通道没有新行,推送 {3} 行" -f ` |
| | | $tablesSeen, $seenChannels.Count, $emptyChannels, $sentRows) |
| | | if ($tablesSeen -eq 0) { |
| | | # 一张都没扫到通常是库列表不对(而不是这些库真的空):把库名念出来,便于现场判断 |
| | | Write-Log (" 提示:这些库里都没有 Ws*_IV_0 表 —— " + (($databases | Select-Object -First 8) -join ', ')) |
| | | } |
| | | return $sentRows |
| | | } |
| | | |