CaiWuXiTong/scripts/erp-daily-bills/run-erp-daily.ps1

232 lines
13 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#
聚胜算每日账单 → 财务系统(统一入口)
平台:toutiao = 头条放心购→资金流水(落 抖音账户)
jd = 京东商城→京东日账单(一次导入按店铺名拆 京东账单 + 京喜账单)
jd-wallet = 京东商城→京东钱包(落 京东网银钱包账户)
流程:导出 → xlsx 转 CSV → 与已导入期间比对(无差异则跳过)→ 上传 → 受控导入 → 页面回验 → 归档 → 写日志
用法:
pwsh -File run-erp-daily.ps1 # 三个平台,区间「月初 → 昨日」
pwsh -File run-erp-daily.ps1 -Platform jd -Through 2026-07-31
pwsh -File run-erp-daily.ps1 -Platform jd-wallet -Through 2026-07-31
pwsh -File run-erp-daily.ps1 -Platform toutiao -SkipImport
#>
[CmdletBinding()]
param(
[ValidateSet('toutiao', 'jd', 'jd-wallet', 'both')][string]$Platform = 'both',
[string]$Through,
[string]$Start,
[switch]$SkipImport,
[switch]$ForceImport
)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$self = (Get-Process -Id $PID).Path
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {}
. (Join-Path $root '..\deploy-auth.ps1')
$node = 'C:\Users\Administrator\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'
$python = 'C:\Users\Administrator\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe'
$exportDir = Join-Path $root '..\..\runtime\private\erp-exports'
$logDir = Join-Path $exportDir 'logs'
$stateFile = Join-Path $exportDir 'imported-state.json'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$logFile = Join-Path $logDir ((Get-Date -Format 'yyyyMMdd') + '.log')
function Write-Log {
param([string]$Message)
$line = '{0} {1}' -f (Get-Date -Format 'HH:mm:ss'), $Message
Write-Host $line
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
}
# 平台 → 导入命令 / 页面回验页签
$specs = @{
toutiao = @{ label = '头条放心购'; artifact = 'douyin'; importCmd = 'business-mirror:import-douyin-authoritative'; verifyTabs = @('douyin'); split = $false }
jd = @{ label = '京东日账单'; artifact = 'jd'; importCmd = 'business-mirror:import-jd-daily'; verifyTabs = @('jd-transactions', 'jingxi'); split = $true }
'jd-wallet' = @{ label = '京东钱包'; artifact = 'jd-wallet'; importCmd = 'business-mirror:import-jd-wallet'; verifyTabs = @('jd-wallet'); split = $false }
}
$targets = if ($Platform -eq 'both') { @('toutiao', 'jd', 'jd-wallet') } else { @($Platform) }
$state = if (Test-Path -LiteralPath $stateFile) { (Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json) } else { New-Object psobject }
$auth = Start-CaiwuDeploymentAuth -Server '218.93.190.75' -SshUser 'root'
$sshOptions = $auth.Options + @('-o', 'PreferredAuthentications=password', '-o', 'PubkeyAuthentication=no', '-o', 'NumberOfPasswordPrompts=1', '-o', 'ConnectTimeout=15')
. (Join-Path $root 'erp-remote.ps1')
$remoteSshScript = Join-Path $root '..\..\runtime\private\server-ssh.ps1'
$remoteUploaderScript = Join-Path $root '..\..\runtime\private\upload-to-server.ps1'
# 远程执行包装:失败自动重试(含重新上传 CSV),并返回 { Ok; Output; Attempts }
function Invoke-Remote {
param(
[Parameter(Mandatory = $true)][string]$Body,
[string]$LocalFile = '',
[string]$RemoteFile = ''
)
return Invoke-ErpRemote `
-PwshPath $self `
-SshScript $remoteSshScript `
-UploaderScript $remoteUploaderScript `
-Body $Body `
-LocalFile $LocalFile `
-RemoteFile $RemoteFile `
-Log { param($message) Write-Log $message }
}
$failed = $false
try {
foreach ($target in $targets) {
$spec = $specs[$target]
Write-Log ('=== ' + $spec.label + ':开始 ===')
# 1) 导出(胜算页面偶发日历面板/授权弹窗/胜算标签页注册慢,失败后自动重试)
$exportArgs = @((Join-Path $root 'export-daily.mjs'), ('--platform=' + $target), ('--out=' + $exportDir))
if ($Start) { $exportArgs += ('--start=' + $Start) }
if ($Through) { $exportArgs += ('--through=' + $Through) }
# 2 次不够:2026-10-12 两次尝试全败、只能人工补跑;2026-10-13 头条/京东各失败 1 次后靠第 2 次成功。
# 单次约 1 分钟,给到 3 次仍远小于 09:30 任务窗口。
$exportAttempts = 3
$exportOk = $false
for ($exportTry = 1; $exportTry -le $exportAttempts -and -not $exportOk; $exportTry++) {
$exportOut = (& $node @exportArgs 2>&1 | Out-String)
Write-Log ('导出:' + ($exportOut.Trim() -replace "`r?`n", ' | '))
if ($LASTEXITCODE -eq 0) {
$exportOk = $true
break
}
Write-Log ($spec.label + ' 导出失败(exit=' + $LASTEXITCODE + ',第 ' + $exportTry + '/' + $exportAttempts + ' 次)')
if ($exportTry -lt $exportAttempts) { Start-Sleep -Seconds 20 }
}
if (-not $exportOk) { $failed = $true; continue }
$summary = Get-Content -LiteralPath (Join-Path $exportDir 'last-export.json') -Raw | ConvertFrom-Json
if ($summary.empty) {
Write-Log ($spec.label + ':该区间胜算暂无数据,跳过(未改动生产)')
continue
}
# 胜算超过同步导出上限时会拆成多个分片(summary.files),逐片转 CSV 后合并成一份再导入
$rawFiles = @()
if (($summary.PSObject.Properties.Name -contains 'files') -and $summary.files) { $rawFiles = @($summary.files) }
if ($rawFiles.Count -eq 0 -and $summary.file) { $rawFiles = @($summary.file) }
if ($rawFiles.Count -eq 0) { Write-Log ($spec.label + ' 导出结果里没有文件名'); $failed = $true; continue }
$rawFile = $rawFiles[0]
# 多分片时 CSV 用合并后的名字(不带 _partN),避免看起来只落了第一片
$csvFile = if ($rawFiles.Count -gt 1) {
[IO.Path]::ChangeExtension(($rawFile -replace '_part\d+(?=\.[^.]+$)', ''), '.csv')
} else {
[IO.Path]::ChangeExtension($rawFile, '.csv')
}
if ($rawFiles.Count -gt 1) {
Write-Log ('导出分片 {0} 个:{1}' -f $rawFiles.Count, (($rawFiles | ForEach-Object { [IO.Path]::GetFileName($_) }) -join ', '))
$conv = (& $python -X utf8 (Join-Path $root 'xlsx-to-csv.py') '--merge' $csvFile @rawFiles 2>&1 | Out-String)
} else {
$conv = (& $python -X utf8 (Join-Path $root 'xlsx-to-csv.py') $rawFile $csvFile 2>&1 | Out-String)
}
$line = (($conv -split "`n") | Where-Object { $_ -match '^csv=' } | Select-Object -First 1)
if (-not $line) { Write-Log ($spec.label + ' 转 CSV 失败:' + ($conv.Trim() -replace "`r?`n", ' | ')); $failed = $true; continue }
$rows = [int]([regex]::Match($line, 'rows=(\d+)').Groups[1].Value)
$income = [double]([regex]::Match($line, 'income=([\-\d\.]+)').Groups[1].Value)
$expense = [double]([regex]::Match($line, 'expense=([\-\d\.]+)').Groups[1].Value)
$month = $summary.monthStart.Substring(0, 7)
Write-Log ('核对数:{0} ~ {1},笔数 {2},收入 {3},支出 {4}' -f $summary.monthStart, $summary.through, $rows, $income, $expense)
($conv -split "`n") | Where-Object { $_ -match '^part=' } | ForEach-Object { Write-Log (' ' + $_.Trim()) }
# 页面笔数 vs 导出行数:不一致说明分片没下全,跳过导入,避免只导入一部分
$pageCount = if ($summary.pageCount) { [int]$summary.pageCount } else { 0 }
if ($pageCount -gt 0 -and $pageCount -ne $rows -and -not $ForceImport) {
Write-Log ('跳过导入:页面笔数 {0} 与导出行数 {1} 不一致(分片可能未下全),未改动生产' -f $pageCount, $rows)
$failed = $true
continue
}
# 2) 无差异跳过
$key = $target + ':' + $month
$prev = $state.$key
$same = $prev -and ([string]$prev.through -eq [string]$summary.through) -and ([int]$prev.rows -eq $rows) -and ([math]::Abs([double]$prev.income - $income) -lt 0.005) -and ([math]::Abs([double]$prev.expense - $expense) -lt 0.005)
if ($same -and -not $ForceImport) {
Write-Log ('该月已导入且无差异({0} 笔 / {1} / {2}),跳过导入' -f $rows, $income, $expense)
continue
}
if ($SkipImport) { Write-Log '已指定 -SkipImport,跳过导入'; continue }
# 3) 上传 + 导入 + 页面回验(远端失败/断连自动重试;整段失败则本平台失败,不写状态、不归档)
$remoteFile = '/tmp/' + $target + '_daily_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.csv'
$upload = Invoke-ErpRemoteUpload -PwshPath $self -UploaderScript $remoteUploaderScript -LocalFile $csvFile -RemoteFile $remoteFile
Write-Log ('上传:' + ($upload.Output.Trim() -replace "`r?`n", ' | '))
if (-not $upload.Ok) {
Write-Log ($spec.label + ' 上传失败:未写入本地状态、未归档(下次运行会重新尝试)')
$failed = $true
continue
}
$verifyPhp = @"
cat > /tmp/verify_daily.php <<'PHP'
<?php
require '/var/www/html/vendor/autoload.php';
`$app = require '/var/www/html/bootstrap/app.php';
`$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
`$repo = app(Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository::class);
`$tabs = ['__TABS__'];
foreach (`$tabs as `$tab) {
`$r = match (`$tab) {
'douyin' => `$repo->paginate(['start' => '__START__', 'end' => '__THROUGH__'], 'occurred_at', 'desc', 1, 1),
'jd-wallet' => app(Modules\EcommerceFinance\Repositories\StoreReceivingJdWalletBillRepository::class)->paginate(['start' => '__START__', 'end' => '__THROUGH__'], 'occurred_at', 'desc', 1, 1),
default => app(Modules\EcommerceFinance\Repositories\JdDailyBillRepository::class)->paginate(['start' => '__START__', 'end' => '__THROUGH__'], 'occurred_at', 'desc', 1, 1, `$tab),
};
echo 'PAGE ' . `$tab . ' total=' . `$r['total'] . ' ' . json_encode(`$r['summary'], JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
PHP
docker exec -i -u www-data caiwu_app php < /tmp/verify_daily.php
rm -f /tmp/verify_daily.php
"@
$verifyPhp = $verifyPhp.Replace('__TABS__', ($spec.verifyTabs -join "','")).Replace('__START__', $summary.monthStart).Replace('__THROUGH__', $summary.through)
$body = @"
set -e
docker cp $remoteFile caiwu_app:/tmp/daily_bills.csv
/usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php /var/www/html/artisan $($spec.importCmd) /tmp/daily_bills.csv $month --through=$($summary.through) --count=$rows --income=$income --expense=$expense
$verifyPhp
rm -f /tmp/daily_bills.csv $remoteFile
"@
$import = Invoke-Remote -Body $body -LocalFile $csvFile -RemoteFile $remoteFile
Write-Log ('导入/回验:' + ($import.Output.Trim() -replace "`r?`n", ' | '))
if (-not $import.Ok) {
Write-Log ($spec.label + ' 导入/回验失败(已尝试 ' + $import.Attempts + ' 次):未写入本地状态、未归档,下次运行会重新导入')
$failed = $true
continue
}
# 4) 记录状态 + 归档
$state | Add-Member -NotePropertyName $key -NotePropertyValue ([pscustomobject]@{ rows = $rows; income = $income; expense = $expense; through = $summary.through; file = $csvFile; at = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') }) -Force
$state | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $stateFile -Encoding UTF8
$monthNumber = [int]$month.Substring(5, 2)
$shareDir = Join-Path 'Z:\高祝兰\财务系统' ('{0}月' -f $monthNumber)
if (Test-Path -LiteralPath 'Z:\高祝兰\财务系统') {
try {
New-Item -ItemType Directory -Path $shareDir -Force | Out-Null
foreach ($f in $rawFiles) {
Copy-Item -LiteralPath $f -Destination (Join-Path $shareDir ([IO.Path]::GetFileName($f))) -Force
}
Copy-Item -LiteralPath $csvFile -Destination (Join-Path $shareDir ([IO.Path]::GetFileName($csvFile))) -Force
Write-Log '已归档到共享盘'
} catch {
Write-Log ('共享盘归档失败:' + $_.Exception.Message)
}
}
Write-Log ('=== ' + $spec.label + ':完成 ===')
}
if ($failed) {
Write-Log '本次运行存在失败平台(详见上面日志):相关平台未写状态、未归档'
exit 1
}
} finally {
Stop-CaiwuDeploymentAuth $auth
}