232 lines
13 KiB
PowerShell
232 lines
13 KiB
PowerShell
<#
|
||
聚胜算每日账单 → 财务系统(统一入口)
|
||
|
||
平台: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
|
||
}
|