$months * @return array */ public function sync( string $from, string $to, int $companyId, array $months = [], bool $dryRun = false, ): array { [$fromDate, $toDate] = $this->dateRange($from, $to); if ($companyId <= 0) { throw new InvalidArgumentException('公司 ID 无效。'); } $coverage = $this->labelCoverage->assertCovered( $fromDate->toDateString(), $toDate->toDateString(), $companyId, ); $months = $this->months($months, $toDate); $this->assertWorkOrderSourceReady(); if ($dryRun) { return [ 'status' => 'ready', 'from' => $fromDate->toDateString(), 'to' => $toDate->toDateString(), 'checked_days' => count($coverage), 'months' => $months, 'dry_run' => true, ]; } $plantingSync = []; foreach ($months as $month) { $plantingSync[$month] = $this->plantingWorkOrders->sync( $month . '-01', $this->monthEnd($month), $companyId, ); } return [ 'status' => 'ready_to_refresh', 'from' => $fromDate->toDateString(), 'to' => $toDate->toDateString(), 'checked_days' => count($coverage), 'months' => $months, 'planting_sync' => $plantingSync, 'dry_run' => false, ]; } /** @return list */ private function months(array $requested, Carbon $through): array { if ($requested === []) { return [ $through->copy()->subMonthNoOverflow()->format('Y-m'), $through->format('Y-m'), ]; } $months = []; foreach ($requested as $month) { $month = trim((string) $month); $normalized = null; if (preg_match('/^\d{4}-\d{2}$/', $month) === 1) { try { $normalized = Carbon::createFromFormat('!Y-m', $month, 'Asia/Shanghai')->format('Y-m'); } catch (Throwable) { $normalized = null; } } if ($normalized !== $month) { throw new InvalidArgumentException('月份格式无效,应为 YYYY-MM:' . $month); } $months[$month] = true; } $months = array_keys($months); sort($months); return $months; } private function assertWorkOrderSourceReady(): void { $mirror = $this->databases->connection( (string) config('upstream_mirrors.connection', 'business_mirror'), ); $schema = $mirror->getSchemaBuilder(); if (! $schema->hasTable('business_mirror_table_states')) { throw new RuntimeException('聚水潭协同工单镜像同步状态表尚未建立。'); } $state = $mirror->table('business_mirror_table_states') ->where('dataset_key', self::WORK_ORDER_DATASET) ->first(['status', 'initial_completed_at']); if ($state === null || $state->initial_completed_at === null) { throw new RuntimeException('聚水潭协同工单镜像尚未完成首次同步。'); } if ((string) $state->status !== 'succeeded') { throw new RuntimeException('聚水潭协同工单镜像最近同步未成功。'); } } /** @return array{0:Carbon,1:Carbon} */ private function dateRange(string $from, string $to): array { $fromDate = $this->date($from); $toDate = $this->date($to); if ($fromDate === null || $toDate === null || $fromDate->gt($toDate)) { throw new InvalidArgumentException('日期范围无效,应为 from <= to,格式 YYYY-MM-DD。'); } return [$fromDate, $toDate]; } private function date(string $value): ?Carbon { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) { return null; } try { $date = Carbon::createFromFormat('!Y-m-d', $value, 'Asia/Shanghai'); } catch (Throwable) { return null; } return $date->format('Y-m-d') === $value ? $date : null; } private function monthEnd(string $month): string { return Carbon::createFromFormat('!Y-m', $month, 'Asia/Shanghai') ->endOfMonth() ->toDateString(); } }