format('Y-m-d'); } /** 取单范围:待处理(锁单前,默认)/ 处理中(已锁单)/ 两者。 */ public const SCOPES = [ 'pending' => '待处理(锁单前)', 'locked' => '处理中(已锁单)', 'all' => '待处理 + 处理中', ]; /** @return list */ public static function scopeStatuses(string $scope): array { return match ($scope) { 'locked' => [Repository::STATUS_LOCKED], 'all' => Repository::OPEN_STATUSES, default => [Repository::STATUS_PENDING], }; } /** 默认付款起始日:报销日往前 7 天。 */ public static function defaultPaidFrom(string $date): string { return self::parseDate($date)->subDays(7)->format('Y-m-d'); } /** @return array{from:string,to:string} 订单付款时间窗口 [起始日 00:00:00, 报销日前一天 23:59:59] */ public static function paidWindow(string $date, ?string $paidFrom = null): array { $day = self::parseDate($date); $from = $paidFrom === null || $paidFrom === '' ? $day->subDays(7) : self::parseDate($paidFrom); if ($from->greaterThanOrEqualTo($day) || $from->lessThan($day->subDays(31))) { throw new InvalidArgumentException('付款起始日须在报销日前 1~31 天内。'); } return [ 'from' => $from->format('Y-m-d 00:00:00'), 'to' => $day->subDay()->format('Y-m-d 23:59:59'), ]; } public static function parseDate(string $date): CarbonImmutable { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1 || $date < '2026-09-09') { throw new InvalidArgumentException('报销日期无效。'); } $parsed = CarbonImmutable::createFromFormat('!Y-m-d', $date); if ($parsed === false || $parsed->format('Y-m-d') !== $date) { throw new InvalidArgumentException('报销日期无效。'); } return $parsed; } /** * @param list|null $statuses 默认只取「待处理」 * @return array{rows:list>,summary:array,window:array{from:string,to:string},uploads:array} */ public function build(int $companyId, string $date, ?string $paidFrom = null, ?array $statuses = null): array { $window = self::paidWindow($date, $paidFrom); $rows = $this->fillErpLabels($this->repository->pendingRows($window['from'], $window['to'], $statuses ?? [Repository::STATUS_PENDING])); $refs = [ 'reimbursed' => $this->repository->reimbursedOrders($date, array_column($rows, 'work_order_no')), 'commission_people' => $this->references->nameSet($companyId, PlantingReimbursementReferences::KIND_COMMISSION), 'roster' => $this->references->nameSet($companyId, PlantingReimbursementReferences::KIND_ROSTER), 'roster_exceptions' => self::ROSTER_EXCEPTIONS, 'profit_centers' => $this->profitCenters($companyId, array_unique(array_column($rows, 'shop_name'))), 'monitor' => $this->references->monitor($companyId), ]; return $this->auditor->audit($rows, $refs, $date) + [ 'window' => $window, 'uploads' => $this->references->uploads($companyId), ]; } /** * 工单里 ERP订单标签为空的(如部分以旧换新单),按线上订单号补查聚水潭订单镜像的标签。 * * @param list> $rows * @return list> */ private function fillErpLabels(array $rows): array { $missing = []; foreach ($rows as $row) { $orderNo = trim((string) ($row['online_order_no'] ?? '')); if ($orderNo !== '' && trim((string) ($row['erp_order_label'] ?? '')) === '') { $missing[] = $orderNo; } } if ($missing === []) { return $rows; } $labels = []; try { $connection = app(GenericBusinessMirrorRepository::class)->connection(); if ($connection->getSchemaBuilder()->hasTable(self::ERP_ORDER_TABLE)) { // so_id 是字符串列,必须按字符串绑定,否则走不了索引。 foreach (array_chunk(array_values(array_unique(array_map('strval', $missing))), 500) as $chunk) { foreach ($connection->table(self::ERP_ORDER_TABLE)->whereIn('so_id', $chunk)->get(['so_id', 'labels']) as $order) { $label = trim((string) $order->labels); if ($label !== '') { $labels[(string) $order->so_id] = isset($labels[(string) $order->so_id]) ? $labels[(string) $order->so_id] . ',' . $label : $label; } } } } } catch (Throwable $exception) { report($exception); } foreach ($rows as &$row) { $orderNo = trim((string) ($row['online_order_no'] ?? '')); if (trim((string) ($row['erp_order_label'] ?? '')) === '' && isset($labels[$orderNo])) { $row['erp_order_label'] = $labels[$orderNo]; } } unset($row); return $rows; } /** * 店铺 → 利润中心:上传的店铺信息 > 店铺维度报表 ERP 店铺资料 > 系统店铺资料。 * * @param list $shops * @return array */ public function profitCenters(int $companyId, array $shops): array { $uploaded = $this->references->shopCenters($companyId); $map = []; $missing = []; foreach ($shops as $shop) { $shop = (string) $shop; if (($uploaded[$shop] ?? '') !== '') { $map[$shop] = $uploaded[$shop]; } else { $missing[] = $shop; } } if ($missing === []) { return $map; } try { $table = app(BusinessMirrorDatasetRegistry::class)->get(\App\Business\PlantingWorkOrderSynchronizer::SHOP_INFO_DATASET)->targetTable(); $connection = app(GenericBusinessMirrorRepository::class)->connection(); if ($connection->getSchemaBuilder()->hasTable($table)) { foreach ($connection->table($table)->whereIn('erpShopName', $missing)->where('responsibleDepartment', '<>', '') ->get(['erpShopName', 'responsibleDepartment']) as $row) { $map[trim((string) $row->erpShopName)] ??= trim((string) $row->responsibleDepartment); } } } catch (Throwable $exception) { report($exception); } $missing = array_values(array_filter($missing, static fn (string $shop): bool => ($map[$shop] ?? '') === '')); if ($missing !== []) { foreach (StoreInformation::query()->where('company_id', $companyId)->whereIn('erp_store_name', $missing) ->where('profit_center', '<>', '')->get(['erp_store_name', 'profit_center']) as $store) { $map[(string) $store->erp_store_name] ??= trim((string) $store->profit_center); } } return array_filter($map, static fn (string $center): bool => $center !== ''); } }