CaiWuXiTong/app/Console/Commands/ImportVipshopJxBankPayments.php

187 lines
7.3 KiB
PHP
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.

<?php
namespace App\Console\Commands;
use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Illuminate\Database\DatabaseManager;
use JsonException;
use Modules\EcommerceFinance\Repositories\VipshopJxBankPaymentRepository;
use RuntimeException;
use Throwable;
class ImportVipshopJxBankPayments extends Command
{
protected $signature = 'vipshop-jx:import-bank-payments
{file : 唯品会结算平台“账单收付款”JSON}
{--replace : 用本次快照替换已有银行卡结算记录}
{--dry-run : 只校验,不写数据库}';
protected $description = '导入唯品会JX账单收付款中实际结算到银行卡的记录';
public function handle(DatabaseManager $databases): int
{
$path = realpath((string) $this->argument('file'));
if ($path === false || ! is_file($path) || ! is_readable($path)) {
$this->error('账单收付款文件不存在或不可读。');
return self::FAILURE;
}
try {
$sourceRows = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
$this->error('账单收付款文件不是有效 JSON。');
return self::FAILURE;
}
if (! is_array($sourceRows) || $sourceRows === []) {
$this->error('账单收付款文件为空。');
return self::FAILURE;
}
try {
[$rows, $skipped] = $this->normalize($sourceRows, hash_file('sha256', $path));
} catch (RuntimeException $exception) {
$this->error($exception->getMessage());
return self::FAILURE;
}
$connection = $databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
if (! $connection->getSchemaBuilder()->hasTable(VipshopJxBankPaymentRepository::TABLE)) {
$this->error('唯品会JX银行卡结算表尚未完成迁移。');
return self::FAILURE;
}
$net = '0.000000';
$byPeriod = [];
foreach ($rows as $row) {
$net = bcadd($net, $row['amount'], 6);
$byPeriod[$row['bill_period']] = bcadd($byPeriod[$row['bill_period']] ?? '0', $row['amount'], 6);
}
ksort($byPeriod);
$this->table(['状态', '源行数', '导入行数', '跳过行数', '净额'], [[
$this->option('dry-run') ? 'validated' : 'ready',
count($sourceRows),
count($rows),
$skipped,
$net,
]]);
$this->table(
['账单期间', '银行卡结算净额'],
array_map(static fn (string $period, string $amount): array => [$period, $amount], array_keys($byPeriod), array_values($byPeriod)),
);
if ($this->option('dry-run')) {
return self::SUCCESS;
}
try {
$connection->transaction(function () use ($connection, $rows): void {
if ($this->option('replace')) {
$connection->table(VipshopJxBankPaymentRepository::TABLE)->delete();
}
$now = now();
foreach (array_chunk($rows, 500) as $chunk) {
$connection->table(VipshopJxBankPaymentRepository::TABLE)->upsert(
array_map(static fn (array $row): array => $row + [
'imported_at' => $now,
'created_at' => $now,
'updated_at' => $now,
], $chunk),
['source_payment_id'],
[
'source_bill_number',
'bill_period',
'pay_date',
'batch_number',
'vendor_account_num',
'amount',
'currency_code',
'pay_status',
'source_hash',
'imported_at',
'updated_at',
],
);
}
});
} catch (Throwable $exception) {
report($exception);
$this->error('唯品会JX银行卡结算写入失败,详细原因已记录到受保护日志。');
return self::FAILURE;
}
$this->info('唯品会JX银行卡结算记录导入成功。');
return self::SUCCESS;
}
/**
* @param list<mixed> $sourceRows
* @return array{0:list<array<string,mixed>>,1:int}
*/
private function normalize(array $sourceRows, string $sourceHash): array
{
$rows = [];
$sourceIds = [];
$skipped = 0;
foreach ($sourceRows as $index => $source) {
if (! is_array($source)) {
throw new RuntimeException('第 ' . ($index + 1) . ' 行不是有效对象。');
}
if ((string) ($source['vendorCode'] ?? '') !== '672378') {
throw new RuntimeException('第 ' . ($index + 1) . ' 行不是供应商 672378。');
}
$vendorAccount = trim((string) ($source['vendorAccountNum'] ?? ''));
if (($source['payStatus'] ?? null) !== 'SUCCESS'
|| $vendorAccount === ''
|| $vendorAccount === '银行存款-中转') {
$skipped++;
continue;
}
$sourceId = (int) ($source['id'] ?? 0);
if ($sourceId < 1 || isset($sourceIds[$sourceId])) {
throw new RuntimeException('第 ' . ($index + 1) . ' 行来源 ID 无效或重复。');
}
$sourceIds[$sourceId] = true;
$billNumber = trim((string) ($source['billNumber'] ?? ''));
if (preg_match('/(?:^|[-_])(20\d{2})(\d{2})\d{2}(?:[-_]|$)/', $billNumber, $matches) !== 1) {
throw new RuntimeException("第 " . ($index + 1) . " 行账单编号无法识别账期:{$billNumber}");
}
$amount = $source['billPayAmount'] ?? null;
if (! is_numeric($amount)) {
throw new RuntimeException('第 ' . ($index + 1) . ' 行已结金额无效。');
}
$payDate = $source['payDate'] ?? null;
if (! is_numeric($payDate)) {
throw new RuntimeException('第 ' . ($index + 1) . ' 行付款日期无效。');
}
$rows[] = [
'source_payment_id' => $sourceId,
'source_bill_number' => $billNumber,
'bill_period' => $matches[1] . '-' . $matches[2],
'pay_date' => CarbonImmutable::createFromTimestampMs((int) $payDate, 'Asia/Shanghai')->format('Y-m-d'),
'batch_number' => mb_substr((string) ($source['batchNumber'] ?? ''), 0, 64),
'vendor_account_num' => mb_substr($vendorAccount, 0, 64),
'amount' => number_format((float) $amount, 6, '.', ''),
'currency_code' => mb_substr((string) ($source['currencyCode'] ?? 'CNY'), 0, 8),
'pay_status' => 'SUCCESS',
'source_hash' => $sourceHash,
];
}
if ($rows === []) {
throw new RuntimeException('账单收付款文件中没有可导入的银行卡结算记录。');
}
return [$rows, $skipped];
}
}