yeg
6 天以前 a04222c873e5e55f48c8d33b5c0bc46fa524ac8d
scripts/j22-collector.ps1
@@ -50,18 +50,52 @@
    [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
}
# ---------------------------------------------------------------- 读取位置
@@ -96,7 +130,9 @@
            [void]$reader.GetValues($values)
            [void]$rows.Add($values)
        }
        return $rows
        # 必须用逗号包一层返回:函数返回集合时 PowerShell 会拆一层,只有一行结果时
        # 调用方 foreach 拿到的就不是"行"而是那一行的列值本身,$row[0] 会变成字符串首字符。
        return ,$rows
    }
    finally { $reader.Close() }
}
@@ -122,11 +158,17 @@
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 {
@@ -134,6 +176,7 @@
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 {
@@ -180,7 +223,10 @@
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
}
@@ -196,11 +242,15 @@
    $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"
@@ -248,7 +298,7 @@
            }
            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)
@@ -262,9 +312,15 @@
    }
    # 拓扑:这台机器当前有数据表的通道。端点忽略重复声明,所以每轮都发没有副作用;
    # 它同时是候选"最近数据时间"的来源之外的通道清单(最近数据时间取采样行)。
    # 它同时是候选的通道清单(最近数据时间取采样行,不取上报时间)。
    [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
}