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 * } */ 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, ]; } }