connectionName()); return $schema->hasTable(self::TABLE) && $schema->hasColumns(self::TABLE, [ 'shop_name', 'online_order_no', 'internal_order_no', 'ship_time', 'product_cost', 'matched_freight_fee', 'matched_package_fee', 'matched_bill_fee', ]); } catch (\Throwable) { return false; } } /** @return array{ready:bool,last_synced_at:?string,total:int,shops:list} */ public function status(): array { try { $query = $this->connection()->table(self::TABLE); return [ 'ready' => $this->ready(), 'last_synced_at' => (clone $query)->max('synced_at'), 'total' => (int) (clone $query)->count(), 'shops' => (clone $query)->whereNotNull('shop_name') ->where('shop_name', '<>', '') ->distinct() ->orderBy('shop_name') ->pluck('shop_name') ->map(static fn (mixed $shop): string => trim((string) $shop)) ->filter() ->values() ->all(), ]; } catch (\Throwable) { return [ 'ready' => false, 'last_synced_at' => null, 'total' => 0, 'shops' => [], ]; } } /** * @param array $filters * @return array{rows:list>,total:int,has_more:bool} */ public function paginate(array $filters, int $page, int $limit): array { $query = $this->filtered($this->connection()->table(self::TABLE), $filters); $total = (int) (clone $query)->count(); $rows = $query ->select([ 'shop_name', 'online_order_no', 'internal_order_no', 'order_key', 'ship_time', 'product_cost', 'matched_package_fee', 'matched_freight_fee', 'matched_bill_fee', 'synced_at', ]) ->orderByDesc('ship_time') ->orderByDesc('order_key') ->forPage(max($page, 1), min(max($limit, 1), 100)) ->get() ->map(static fn (object $row): array => [ 'shop_name' => trim((string) ($row->shop_name ?? '')), 'online_order_no' => trim((string) ($row->online_order_no ?? '')), 'internal_order_no' => trim((string) ($row->internal_order_no ?? '')), 'order_key' => trim((string) ($row->order_key ?? '')), 'ship_time' => $row->ship_time, 'product_cost' => $row->product_cost === null ? null : (float) $row->product_cost, 'matched_package_fee' => $row->matched_package_fee === null ? null : (float) $row->matched_package_fee, 'matched_freight_fee' => $row->matched_freight_fee === null ? null : (float) $row->matched_freight_fee, 'matched_bill_fee' => $row->matched_bill_fee === null ? null : (float) $row->matched_bill_fee, 'synced_at' => $row->synced_at, 'anomaly_type' => '商品成本及快递费缺失', ]) ->all(); return [ 'rows' => $rows, 'total' => $total, 'has_more' => $page * min(max($limit, 1), 100) < $total, ]; } private function filtered(\Illuminate\Database\Query\Builder $query, array $filters): \Illuminate\Database\Query\Builder { $query->where(function ($cost) { $cost->whereNull('product_cost')->orWhere('product_cost', 0); })->where(function ($freight) { $freight->whereNull('matched_freight_fee')->orWhere('matched_freight_fee', 0); }); $from = trim((string) ($filters['from'] ?? '')); $to = trim((string) ($filters['to'] ?? '')); if ($from !== '') { $query->where('ship_time', '>=', $from . ' 00:00:00'); } if ($to !== '') { $query->where('ship_time', '<=', $to . ' 23:59:59'); } $shops = array_values(array_unique(array_filter(array_map( static fn (mixed $value): string => trim((string) $value), (array) ($filters['shop_names'] ?? []), )))); if ($shops !== []) { $query->whereIn('shop_name', array_slice($shops, 0, 50)); } $orders = $this->batchValues((string) ($filters['order_no'] ?? '')); if ($orders !== []) { $query->where(function ($order) use ($orders): void { $order->whereIn('online_order_no', $orders) ->orWhereIn('internal_order_no', $orders); }); } return $query; } /** @return list */ private function batchValues(string $value): array { $parts = preg_split('/[\s,,;;]+/u', trim($value)) ?: []; $values = []; foreach ($parts as $part) { $part = trim($part); if ($part !== '') { $values[$part] = true; } if (count($values) >= 100) { break; } } return array_keys($values); } private function connection(): ConnectionInterface { return DB::connection($this->connectionName()); } private function connectionName(): string { return (string) config('upstream_mirrors.connection', 'business_mirror'); } }