295 lines
10 KiB
PHP
295 lines
10 KiB
PHP
<?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,
|
||
];
|
||
}
|
||
}
|