补齐京东钱包每日账单同步并增加覆盖校验
This commit is contained in:
parent
81bf2bd779
commit
144937bfa3
172
app/Console/Commands/VerifyErpDailyBillCoverage.php
Normal file
172
app/Console/Commands/VerifyErpDailyBillCoverage.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class VerifyErpDailyBillCoverage extends Command
|
||||
{
|
||||
protected $signature = 'business-mirror:verify-erp-daily-coverage
|
||||
{--from=2026-07-01 : 起始日期 YYYY-MM-DD}
|
||||
{--to= : 截止日期 YYYY-MM-DD,默认昨日}
|
||||
{--json : 输出 JSON}';
|
||||
|
||||
protected $description = '核对京东日账单、京东钱包、头条放心购的月份与截止日覆盖是否完整';
|
||||
|
||||
private const TARGETS = [
|
||||
'jd-daily' => [
|
||||
'label' => '京东日账单',
|
||||
'periods' => 'jd_daily_bill_periods',
|
||||
'rows' => 'jd_daily_bill_rows',
|
||||
],
|
||||
'jd-wallet' => [
|
||||
'label' => '京东钱包',
|
||||
'periods' => 'jd_wallet_bill_periods',
|
||||
'rows' => 'jd_wallet_bill_rows',
|
||||
],
|
||||
'douyin' => [
|
||||
'label' => '头条放心购',
|
||||
'periods' => 'douyin_authoritative_periods',
|
||||
'rows' => 'douyin_authoritative_bills',
|
||||
],
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$fromText = (string) $this->option('from');
|
||||
$toText = (string) ($this->option('to') ?: now('Asia/Shanghai')->subDay()->toDateString());
|
||||
$from = $this->date($fromText);
|
||||
$to = $this->date($toText);
|
||||
|
||||
if ($from === null || $to === null || $from->gt($to)) {
|
||||
$this->error('日期范围无效,应为 from <= to,格式 YYYY-MM-DD。');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$db = DB::connection(config('upstream_mirrors.connection', 'business_mirror'));
|
||||
$schema = Schema::connection(config('upstream_mirrors.connection', 'business_mirror'));
|
||||
$results = [];
|
||||
$failed = false;
|
||||
|
||||
foreach (self::TARGETS as $key => $target) {
|
||||
if (! $schema->hasTable($target['periods']) || ! $schema->hasTable($target['rows'])) {
|
||||
$results[] = [
|
||||
'dataset' => $key,
|
||||
'label' => $target['label'],
|
||||
'month' => null,
|
||||
'required_start' => $from->toDateString(),
|
||||
'required_through' => $to->toDateString(),
|
||||
'imported_start' => null,
|
||||
'imported_end' => null,
|
||||
'row_count' => 0,
|
||||
'registered_rows' => null,
|
||||
'income' => null,
|
||||
'expense' => null,
|
||||
'status' => 'missing_table',
|
||||
'issue' => '镜像表不存在',
|
||||
];
|
||||
$failed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$month = $from->copy()->startOfMonth();
|
||||
while ($month->lte($to)) {
|
||||
$monthEnd = $month->copy()->endOfMonth();
|
||||
$requiredStart = $month->lt($from) ? $from->copy() : $month->copy();
|
||||
$requiredThrough = $monthEnd->gt($to) ? $to->copy() : $monthEnd->copy();
|
||||
$requiredEndExclusive = $requiredThrough->copy()->addDay();
|
||||
$period = $db->table($target['periods'])
|
||||
->where('period_start', $month->toDateString())
|
||||
->first();
|
||||
|
||||
$status = 'ok';
|
||||
$rowCount = 0;
|
||||
$issue = null;
|
||||
$summary = null;
|
||||
|
||||
if ($period === null) {
|
||||
$status = 'missing_month';
|
||||
$issue = '缺少该月份导入记录';
|
||||
} elseif (Carbon::parse((string) $period->period_end)->lt($requiredEndExclusive)) {
|
||||
$status = 'partial';
|
||||
$issue = '导入截止日早于要求日期';
|
||||
} else {
|
||||
$summary = $db->table($target['rows'])
|
||||
->where('generation', $period->active_generation)
|
||||
->selectRaw('COUNT(*) AS rows_count, COALESCE(SUM(income), 0) AS income, COALESCE(SUM(expense), 0) AS expense')
|
||||
->first();
|
||||
$rowCount = (int) ($summary->rows_count ?? 0);
|
||||
if ($rowCount === 0) {
|
||||
$status = 'empty';
|
||||
$issue = '有效版本没有明细';
|
||||
} elseif ($rowCount !== (int) $period->row_count) {
|
||||
$status = 'row_count_mismatch';
|
||||
$issue = '有效版本实际行数与期间登记不一致';
|
||||
} elseif (abs(round((float) $summary->income, 2) - (float) $period->income) > 0.005
|
||||
|| abs(round((float) $summary->expense, 2) - (float) $period->expense) > 0.005) {
|
||||
$status = 'amount_mismatch';
|
||||
$issue = '有效版本收支合计与期间登记不一致';
|
||||
}
|
||||
}
|
||||
|
||||
if ($status !== 'ok') {
|
||||
$failed = true;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'dataset' => $key,
|
||||
'label' => $target['label'],
|
||||
'month' => $month->format('Y-m'),
|
||||
'required_start' => $requiredStart->toDateString(),
|
||||
'required_through' => $requiredThrough->toDateString(),
|
||||
'imported_start' => $period?->period_start,
|
||||
'imported_end' => $period?->period_end,
|
||||
'row_count' => $rowCount,
|
||||
'registered_rows' => $period === null ? null : (int) $period->row_count,
|
||||
'income' => isset($summary) ? round((float) $summary->income, 2) : null,
|
||||
'expense' => isset($summary) ? round((float) $summary->expense, 2) : null,
|
||||
'status' => $status,
|
||||
'issue' => $issue,
|
||||
];
|
||||
|
||||
unset($summary);
|
||||
$month->addMonthNoOverflow()->startOfMonth();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line(json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
} else {
|
||||
$this->table(
|
||||
['数据集', '月份', '要求覆盖', '导入覆盖', '实际行数', '登记行数', '收入', '支出', '状态', '说明'],
|
||||
array_map(static fn (array $row): array => [
|
||||
$row['label'],
|
||||
$row['month'] ?? '—',
|
||||
($row['required_start'] ?? '—') . ' ~ ' . ($row['required_through'] ?? '—'),
|
||||
($row['imported_start'] ?? '—') . ' ~ ' . ($row['imported_end'] ?? '—'),
|
||||
number_format((int) $row['row_count']),
|
||||
$row['registered_rows'] === null ? '—' : number_format((int) $row['registered_rows']),
|
||||
$row['income'] === null ? '—' : number_format((float) $row['income'], 2),
|
||||
$row['expense'] === null ? '—' : number_format((float) $row['expense'], 2),
|
||||
$row['status'],
|
||||
$row['issue'] ?? '',
|
||||
], $results),
|
||||
);
|
||||
}
|
||||
|
||||
return $failed ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
private function date(string $value): ?Carbon
|
||||
{
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = Carbon::createFromFormat('!Y-m-d', $value, 'Asia/Shanghai');
|
||||
return $date !== false && $date->format('Y-m-d') === $value ? $date : null;
|
||||
}
|
||||
}
|
||||
@ -8,16 +8,20 @@
|
||||
| --- | --- | --- | --- |
|
||||
| `toutiao` | 头条放心购 → 资金流水 | `business-mirror:import-douyin-authoritative` | 账单流水数据 › 店铺收款账户账单 › **抖音账户** |
|
||||
| `jd` | 京东商城 → 京东日账单 | `business-mirror:import-jd-daily` | **京东账单 + 京喜账单**(一次导入按店铺名拆两边,两边合计恒等于导出行数) |
|
||||
| `jd-wallet` | 京东商城 → 京东钱包 | `business-mirror:import-jd-wallet` | **京东网银钱包账户** |
|
||||
|
||||
## 用法
|
||||
|
||||
```powershell
|
||||
# 两个平台都跑:区间「月初 → 昨日」
|
||||
# 三个平台都跑:区间「月初 → 昨日」
|
||||
pwsh -File scripts\erp-daily-bills\run-erp-daily.ps1
|
||||
|
||||
# 只跑京东,补历史月份
|
||||
pwsh -File scripts\erp-daily-bills\run-erp-daily.ps1 -Platform jd -Through 2026-07-31
|
||||
|
||||
# 只跑京东钱包,补历史月份
|
||||
pwsh -File scripts\erp-daily-bills\run-erp-daily.ps1 -Platform jd-wallet -Through 2026-07-31
|
||||
|
||||
# 只导出不导入(排查)
|
||||
pwsh -File scripts\erp-daily-bills\run-erp-daily.ps1 -Platform toutiao -Through 2026-10-05 -SkipImport
|
||||
```
|
||||
@ -26,9 +30,19 @@ pwsh -File scripts\erp-daily-bills\run-erp-daily.ps1 -Platform toutiao -Through
|
||||
- **无差异跳过**:该平台该月已导入且笔数/收入/支出完全一致时跳过导入(`-ForceImport` 可强制重刷);状态记在 `runtime/private/erp-exports/imported-state.json`。
|
||||
- 超过胜算同步导出上限(响应 `超出同步导出允许最大数据量`)时,脚本自动轮询「异步导出下载管理」取回本次任务的全部分片,逐片转 CSV 后合并成一份再导入;仍可用 `-Start/-Through` 分段导出。
|
||||
- 导入前用页面笔数(共 N 条)与导出行数核对,不一致就跳过导入(`-ForceImport` 可强制),避免只导入一部分。
|
||||
- 稳定性:导出失败自动重试一次(胜算页面偶发日历面板/授权弹窗异常);上传与"导入+回验"失败自动重试 3 次(含重新上传 CSV),用于吸收服务器 SSH 偶发断连。
|
||||
- 稳定性:导出最多重试 3 次(胜算页面偶发日历面板/授权弹窗异常);上传与"导入+回验"失败自动重试 3 次(含重新上传 CSV),用于吸收服务器 SSH 偶发断连。
|
||||
- 失败即失败:导出/转 CSV/笔数核对/上传/导入回验任一步失败,都会**不写本地状态、不归档**,并在结束时以退出码 1 结束(下次运行会重新导入),不会把失败当成功记入 `imported-state.json`。
|
||||
|
||||
## 覆盖校验
|
||||
|
||||
导入后可核对三个数据集在指定区间内是否逐月覆盖到截止日:
|
||||
|
||||
```powershell
|
||||
docker exec -u www-data caiwu_app php artisan business-mirror:verify-erp-daily-coverage --from=2026-07-01 --to=2026-10-19
|
||||
```
|
||||
|
||||
命令会逐个月份检查期间起止范围、有效版本行数和登记行数;缺月、只导到月中或行数不一致都会返回失败。
|
||||
|
||||
## 组成
|
||||
|
||||
| 文件 | 作用 |
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
*
|
||||
* --platform=toutiao 头条放心购 → 资金流水 (导入 import-douyin-authoritative → 抖音账户)
|
||||
* --platform=jd 京东商城 → 京东日账单 (导入 import-jd-daily → 京东账单 + 京喜账单,按店铺名拆分)
|
||||
* --platform=jd-wallet 京东商城 → 京东钱包 (导入 import-jd-wallet → 京东网银钱包账户)
|
||||
*
|
||||
* 用法:
|
||||
* node export-daily.mjs --platform=toutiao # 本月月初 → 昨日
|
||||
@ -24,6 +25,7 @@ const BASE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PLATFORMS = {
|
||||
toutiao: { label: '头条放心购', subTab: '资金流水', filePrefix: '头条放心购_资金流水', exportMainName: '头条放心购头条资金流水' },
|
||||
jd: { label: '京东商城', subTab: '京东日账单', filePrefix: '京东日账单', exportMainName: '京东商城京东日账单' },
|
||||
'jd-wallet': { label: '京东商城', subTab: '京东钱包', filePrefix: '京东钱包', exportMainName: '京东商城京东钱包' },
|
||||
};
|
||||
const arg = (name, fallback = null) => {
|
||||
const hit = process.argv.find((a) => a.startsWith(`--${name}=`));
|
||||
@ -41,7 +43,7 @@ const BILL_URL = 'https://ss.erp321.com/process/platform-bill-new';
|
||||
const platformKey = arg('platform', 'toutiao');
|
||||
const platform = PLATFORMS[platformKey];
|
||||
if (!platform) {
|
||||
throw new Error('未知平台:' + platformKey + '(可选 toutiao / jd)');
|
||||
throw new Error('未知平台:' + platformKey + '(可选 toutiao / jd / jd-wallet)');
|
||||
}
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
@ -3,17 +3,19 @@
|
||||
|
||||
平台:toutiao = 头条放心购→资金流水(落 抖音账户)
|
||||
jd = 京东商城→京东日账单(一次导入按店铺名拆 京东账单 + 京喜账单)
|
||||
jd-wallet = 京东商城→京东钱包(落 京东网银钱包账户)
|
||||
|
||||
流程:导出 → xlsx 转 CSV → 与已导入期间比对(无差异则跳过)→ 上传 → 受控导入 → 页面回验 → 归档 → 写日志
|
||||
|
||||
用法:
|
||||
pwsh -File run-erp-daily.ps1 # 两个平台,区间「月初 → 昨日」
|
||||
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', 'both')][string]$Platform = 'both',
|
||||
[ValidateSet('toutiao', 'jd', 'jd-wallet', 'both')][string]$Platform = 'both',
|
||||
[string]$Through,
|
||||
[string]$Start,
|
||||
[switch]$SkipImport,
|
||||
@ -45,9 +47,10 @@ function Write-Log {
|
||||
$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') } else { @($Platform) }
|
||||
$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'
|
||||
@ -144,7 +147,7 @@ try {
|
||||
# 2) 无差异跳过
|
||||
$key = $target + ':' + $month
|
||||
$prev = $state.$key
|
||||
$same = $prev -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)
|
||||
$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
|
||||
@ -170,9 +173,11 @@ require '/var/www/html/vendor/autoload.php';
|
||||
`$repo = app(Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository::class);
|
||||
`$tabs = ['__TABS__'];
|
||||
foreach (`$tabs as `$tab) {
|
||||
`$r = `$tab === 'douyin'
|
||||
? `$repo->paginate(['start' => '__START__', 'end' => '__THROUGH__'], 'occurred_at', 'desc', 1, 1)
|
||||
: app(Modules\EcommerceFinance\Repositories\JdDailyBillRepository::class)->paginate(['start' => '__START__', 'end' => '__THROUGH__'], 'occurred_at', 'desc', 1, 1, `$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
|
||||
@ -197,12 +202,14 @@ rm -f /tmp/daily_bills.csv $remoteFile
|
||||
}
|
||||
|
||||
# 4) 记录状态 + 归档
|
||||
$state | Add-Member -NotePropertyName $key -NotePropertyValue ([pscustomobject]@{ rows = $rows; income = $income; expense = $expense; file = $csvFile; at = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') }) -Force
|
||||
$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
|
||||
|
||||
$shareDir = 'Z:\高祝兰\财务系统\9月'
|
||||
$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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user