187 lines
7.3 KiB
PHP
187 lines
7.3 KiB
PHP
<?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];
|
||
}
|
||
}
|