CaiWuXiTong/app/Business/InventoryWarehouseMappingAlerts.php

295 lines
10 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\Business;
use App\Models\Business\SystemizationTodo;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\InventoryReportAuthoritativeImporter;
use RuntimeException;
class InventoryWarehouseMappingAlerts
{
public const CATEGORY = '商品价格管理';
public const SECTION = '进销存报表';
private const SOURCE_KEY_PREFIX = 'inventory-warehouse-unmapped:';
private readonly string $mirrorConnection;
public function __construct(
private readonly DatabaseManager $databases,
private readonly InventoryWarehouseShortNames $shortNames,
) {
$this->mirrorConnection = (string) config('upstream_mirrors.connection', 'business_mirror');
}
public function latestMonth(): ?string
{
$connection = $this->databases->connection($this->mirrorConnection);
$schema = Schema::connection($this->mirrorConnection);
if (! $schema->hasTable('inventory_report_authoritative_months')) {
return null;
}
$month = $connection->table('inventory_report_authoritative_months')
->where('dataset_key', InventoryReportAuthoritativeImporter::DATASET_KEY)
->where('status', 'succeeded')
->whereNotNull('active_generation')
->orderByDesc('source_month')
->value('source_month');
return $month === null
? null
: Carbon::parse($month)->startOfMonth()->toDateString();
}
/**
* @return array{
* status:string,
* month:string,
* missing_pair_count:int,
* missing_row_count:int,
* task_id:?int,
* action:string
* }
*/
public function syncMonth(string $month, int $companyId = 1): array
{
$month = Carbon::parse($month)->startOfMonth()->toDateString();
$lockKey = sprintf('inventory-warehouse-mapping-alert:%d:%s', $companyId, $month);
return Cache::lock($lockKey, 120)->block(20, function () use ($month, $companyId): array {
return $this->syncMonthWithoutLock($month, $companyId);
});
}
/**
* @return array{
* status:string,
* month:string,
* source_rows:int,
* missing_pair_count:int,
* missing_row_count:int,
* missing:list<array{warehouse_provider:string,warehouse:string,source_rows:int}>
* }
*/
public function missingForMonth(string $month, int $companyId = 1): array
{
$month = Carbon::parse($month)->startOfMonth()->toDateString();
$connection = $this->databases->connection($this->mirrorConnection);
$schema = Schema::connection($this->mirrorConnection);
foreach (['inventory_report_authoritative_months', 'inventory_report_authoritative_rows'] as $table) {
if (! $schema->hasTable($table)) {
throw new RuntimeException('进销存权威月报表尚未完成迁移:' . $table);
}
}
$state = $connection->table('inventory_report_authoritative_months')
->where('dataset_key', InventoryReportAuthoritativeImporter::DATASET_KEY)
->whereDate('source_month', $month)
->where('status', 'succeeded')
->whereNotNull('active_generation')
->first();
if ($state === null) {
return [
'status' => 'unavailable',
'month' => $month,
'source_rows' => 0,
'missing_pair_count' => 0,
'missing_row_count' => 0,
'missing' => [],
];
}
$mapping = $this->shortNames->forCompany($companyId);
ksort($mapping, SORT_STRING);
$cacheKey = sprintf(
'inventory-warehouse-unmapped:%d:%s:%s:%s',
$companyId,
$month,
(string) $state->active_generation,
hash('sha256', (string) json_encode($mapping, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
);
return Cache::remember($cacheKey, now()->addMinutes(10), function () use (
$connection,
$state,
$month,
$mapping,
): array {
$sourcePairs = $connection->table('inventory_report_authoritative_rows')
->where('generation', (string) $state->active_generation)
->whereDate('report_month', $month)
->select(['warehouse_provider', 'warehouse'])
->selectRaw('COUNT(*) AS source_rows')
->groupBy('warehouse_provider', 'warehouse')
->orderByDesc('source_rows')
->orderBy('warehouse_provider')
->orderBy('warehouse')
->get();
$missing = [];
foreach ($sourcePairs as $pair) {
$provider = trim((string) $pair->warehouse_provider);
$warehouse = trim((string) $pair->warehouse);
$shortName = trim((string) ($mapping[$provider . "\0" . $warehouse] ?? ''));
if ($shortName !== '') {
continue;
}
$missing[] = [
'warehouse_provider' => $provider,
'warehouse' => $warehouse,
'source_rows' => (int) $pair->source_rows,
];
}
return [
'status' => 'ready',
'month' => $month,
'source_rows' => (int) $state->source_row_count,
'missing_pair_count' => count($missing),
'missing_row_count' => array_sum(array_column($missing, 'source_rows')),
'missing' => $missing,
];
});
}
/**
* @return array{
* status:string,
* month:string,
* missing_pair_count:int,
* missing_row_count:int,
* task_id:?int,
* action:string
* }
*/
private function syncMonthWithoutLock(string $month, int $companyId): array
{
$summary = $this->missingForMonth($month, $companyId);
if ($summary['status'] !== 'ready') {
throw new RuntimeException('该月份尚无已激活的进销存权威月报:' . $month);
}
$missing = $summary['missing'];
$sourceKey = self::SOURCE_KEY_PREFIX . substr($month, 0, 7);
$todo = SystemizationTodo::withTrashed()
->allCompanies()
->where('company_id', $companyId)
->where('source_key', $sourceKey)
->first();
if ($todo?->trashed()) {
return [
'status' => 'skipped',
'month' => $month,
'missing_pair_count' => count($missing),
'missing_row_count' => array_sum(array_column($missing, 'source_rows')),
'task_id' => (int) $todo->id,
'action' => 'deleted',
];
}
$monthLabel = Carbon::parse($month)->format('Y年n月');
if ($missing === []) {
if ($todo === null) {
return [
'status' => 'complete',
'month' => $month,
'missing_pair_count' => 0,
'missing_row_count' => 0,
'task_id' => null,
'action' => 'none',
];
}
if ($todo->status !== SystemizationTodo::STATUS_COMPLETED) {
$todo->update([
'status' => SystemizationTodo::STATUS_COMPLETED,
'completed_at' => today()->toDateString(),
'progress' => $monthLabel . ' 进销存报表的仓储方和仓库均已配置仓库简称,自动完成提醒。',
'notes' => null,
]);
return [
'status' => 'complete',
'month' => $month,
'missing_pair_count' => 0,
'missing_row_count' => 0,
'task_id' => (int) $todo->id,
'action' => 'completed',
];
}
return [
'status' => 'complete',
'month' => $month,
'missing_pair_count' => 0,
'missing_row_count' => 0,
'task_id' => (int) $todo->id,
'action' => 'unchanged',
];
}
$missingRowCount = array_sum(array_column($missing, 'source_rows'));
$progress = [
sprintf(
'%s 聚水潭进销存报表发现 %d 个“仓储方 / 仓库”组合未配置仓库简称,共涉及 %d 行。',
$monthLabel,
count($missing),
$missingRowCount,
),
'请前往“商品价格管理 → 进销存报表 → 仓库名称映射表”补充映射。',
'',
];
foreach ($missing as $index => $item) {
$progress[] = sprintf(
'%d. %s / %s(%d 行)',
$index + 1,
$item['warehouse_provider'],
$item['warehouse'],
$item['source_rows'],
);
}
$payload = [
'category' => self::CATEGORY,
'section' => self::SECTION,
'title' => $monthLabel . ' 进销存仓库简称待映射',
'status' => SystemizationTodo::STATUS_INCOMPLETE,
'completed_at' => null,
'progress' => implode("\n", $progress),
'notes' => null,
];
if ($todo === null) {
$todo = SystemizationTodo::allCompanies()->create([
...$payload,
'company_id' => $companyId,
'start_date' => today()->toDateString(),
'sort_order' => (int) SystemizationTodo::allCompanies()
->where('company_id', $companyId)
->max('sort_order') + 1,
'source_key' => $sourceKey,
'imported_at' => null,
]);
$action = 'created';
} else {
$action = $todo->status === SystemizationTodo::STATUS_COMPLETED
? 'reopened'
: 'updated';
$todo->update($payload);
}
return [
'status' => 'missing',
'month' => $month,
'missing_pair_count' => count($missing),
'missing_row_count' => $missingRowCount,
'task_id' => (int) $todo->id,
'action' => $action,
];
}
}