readyCache ??= ($this->fields ?? app(GoodsProfitAfsRepository::class))->ready(); } public function status(): array { try { $state = $this->connection()->table('business_mirror_table_states') ->where('dataset_key', GoodsProfitAfsRepository::MAIN_DATASET) ->first(['initial_completed_at', 'last_succeeded_at', 'last_error_code']); } catch (\Throwable) { $state = null; } return [ 'ready' => $this->ready(), 'initial_completed_at' => $state->initial_completed_at ?? null, 'last_succeeded_at' => $state->last_succeeded_at ?? null, 'last_error_code' => $state->last_error_code ?? null, ]; } /** * @param array $filters * @return array{rows:list>,has_more:bool} */ public function paginate(array $filters, int $page, int $limit): array { if (! $this->hasActiveFilters($filters)) { return ['rows' => [], 'has_more' => false]; } $columns = $this->columns(); foreach ([ 'shop_name', 'online_order_no', 'internal_order_no', 'order_time', 'product_cost', 'refund_type', 'post_refund_amount', 'post_platform_subsidy', 'pre_refund_amount', 'pre_platform_subsidy', 'classification_order', ] as $field) { if (! isset($columns[$field])) { return ['rows' => [], 'has_more' => false]; } } $page = max($page, 1); $limit = min(max($limit, 1), 100); $orderValues = self::batchValues((string) ($filters['order_no'] ?? '')); if ($orderValues !== []) { $ids = $this->orderNumberIds($orderValues, $columns); if ($ids === []) { return ['rows' => [], 'has_more' => false]; } $query = $this->connection()->table(self::TABLE)->whereIn('_mirror_key', $ids); $query = $this->filtered( $query, array_replace($filters, ['order_no' => '', 'from' => '', 'to' => '']), $columns, ); } else { $query = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns); } $shop = $this->quote($columns['shop_name']); $online = $this->quote($columns['online_order_no']); $internal = $this->quote($columns['internal_order_no']); $orderTime = $this->quote($columns['order_time']); $refundType = $this->quote($columns['refund_type']); $categoryOrder = $this->quote($columns['classification_order']); $cost = $this->sumExpression($columns, ['product_cost']); $refund = $this->sumExpression($columns, ['pre_refund_amount', 'post_refund_amount']); $platformSubsidy = $this->sumExpression($columns, ['pre_platform_subsidy', 'post_platform_subsidy']); $quantity = $this->sumExpression($columns, ['product_quantity']); $amount = $this->sumExpression($columns, ['product_amount']); $records = $query ->selectRaw("{$shop} AS shop_name") ->selectRaw("{$online} AS online_order_no") ->selectRaw("{$internal} AS internal_order_no") ->selectRaw("MAX({$orderTime}) AS order_time") ->selectRaw("{$cost} AS cost_amount") ->selectRaw("{$refund} AS refund_amount") ->selectRaw("{$platformSubsidy} AS platform_subsidy") ->selectRaw("{$quantity} AS product_quantity") ->selectRaw("{$amount} AS product_amount") ->selectRaw("MAX(NULLIF(TRIM({$refundType}), '')) AS refund_type") ->selectRaw("MAX(NULLIF(TRIM({$categoryOrder}), '')) AS classification_order") ->selectRaw('COUNT(*) AS source_row_count') ->groupBy($columns['shop_name'], $columns['online_order_no'], $columns['internal_order_no']) ->orderByRaw("MAX({$orderTime}) DESC") ->orderByDesc($columns['internal_order_no']) ->forPage($page, $limit + 1) ->get(); $rows = $records->map(static fn (object $record): array => [ 'shop_name' => trim((string) ($record->shop_name ?? '')), 'online_order_no' => trim((string) ($record->online_order_no ?? '')), 'internal_order_no' => trim((string) ($record->internal_order_no ?? '')), 'order_time' => $record->order_time, 'cost_amount' => round((float) ($record->cost_amount ?? 0), 2), 'refund_amount' => round((float) ($record->refund_amount ?? 0), 2), 'platform_subsidy' => round((float) ($record->platform_subsidy ?? 0), 2), 'product_quantity' => round((float) ($record->product_quantity ?? 0), 2), 'product_amount' => round((float) ($record->product_amount ?? 0), 2), 'refund_type' => $record->refund_type, 'classification_order' => $record->classification_order, 'source_row_count' => (int) ($record->source_row_count ?? 0), 'matched_package_fee' => null, 'matched_freight_fee' => null, 'matched_bill_fee' => null, 'items' => [], ])->all(); $hasMore = count($rows) > $limit; $rows = array_slice($rows, 0, $limit); $this->hydrateItems($rows, $columns); return ['rows' => $rows, 'has_more' => $hasMore]; } /** @return list */ public function shopNames(): array { $cacheKey = 'order-profit.shop-options:undefined8'; $cached = Cache::get($cacheKey); if (is_array($cached)) { return $cached; } if (! $this->ready()) { return []; } $columns = $this->columns(); $shopColumn = $columns['shop_name'] ?? null; if ($shopColumn === null) { return []; } $names = $this->connection()->table(self::TABLE) ->select($shopColumn) ->whereNotNull($shopColumn) ->where($shopColumn, '<>', '') ->distinct() ->orderBy($shopColumn) ->pluck($shopColumn) ->map(static fn (mixed $name): string => trim((string) $name)) ->filter() ->values() ->all(); Cache::put($cacheKey, $names, now()->addHours(24)); return $names; } /** @return array */ private function columns(): array { return ($this->fields ?? app(GoodsProfitAfsRepository::class))->fieldColumns(); } /** * @param list $orderValues * @param array $columns * @return list */ private function orderNumberIds(array $orderValues, array $columns): array { $ids = []; $single = count($orderValues) === 1 ? $orderValues[0] : null; foreach (['online_order_no', 'internal_order_no'] as $field) { if (! isset($columns[$field])) { continue; } if ($field === 'internal_order_no') { $orderValues = array_values(array_filter( $orderValues, static fn (string $value): bool => mb_strlen($value) <= 11, )); if ($orderValues === []) { continue; } } $query = $this->connection()->table(self::TABLE)->select('_mirror_key')->limit(10000); if ($single !== null) { $query->where($columns[$field], 'like', $single . '%'); } else { $query->whereIn($columns[$field], $orderValues); } foreach ($query->pluck('_mirror_key') as $id) { $ids[(string) $id] = true; } } return array_keys($ids); } /** * @param array $filters * @param array $columns */ private function filtered(Builder $query, array $filters, array $columns): Builder { $dateColumn = $this->queryDateColumn($columns); $from = trim((string) ($filters['from'] ?? '')); $to = trim((string) ($filters['to'] ?? '')); if ($dateColumn !== null && $from !== '') { $query->where($dateColumn, '>=', $from . ' 00:00:00'); } if ($dateColumn !== null && $to !== '') { $query->where($dateColumn, '<=', $to . ' 23:59:59'); } $shops = self::batchValues((string) ($filters['shop_name'] ?? '')); if ($shops !== [] && isset($columns['shop_name'])) { $query->whereIn($columns['shop_name'], $shops); } $orderValues = self::batchValues((string) ($filters['order_no'] ?? '')); if ($orderValues !== []) { $query->where(function (Builder $order) use ($orderValues, $columns): void { foreach (['online_order_no', 'internal_order_no'] as $field) { if (! isset($columns[$field])) { continue; } $order->orWhereIn($columns[$field], $orderValues); } }); } $skuValues = self::batchValues((string) ($filters['sku_id'] ?? '')); if ($skuValues !== [] && isset($columns['product_code'])) { $query->whereIn($columns['product_code'], $skuValues); } return $query; } /** * @param list> $rows * @param array $columns */ private function hydrateItems(array &$rows, array $columns): void { if ($rows === []) { return; } $itemFields = array_values(array_filter( self::ITEM_FIELDS, static fn (string $field): bool => isset($columns[$field]), )); if ($itemFields === []) { return; } $onlineValues = array_values(array_filter(array_column($rows, 'online_order_no'))); $internalValues = array_values(array_filter(array_column($rows, 'internal_order_no'))); if ($onlineValues === [] && $internalValues === []) { return; } $query = $this->connection()->table(self::TABLE); $query->where(function (Builder $orders) use ($columns, $onlineValues, $internalValues): void { if ($onlineValues !== [] && isset($columns['online_order_no'])) { $orders->whereIn($columns['online_order_no'], $onlineValues); } if ($internalValues !== [] && isset($columns['internal_order_no'])) { $method = $onlineValues === [] ? 'whereIn' : 'orWhereIn'; $orders->{$method}($columns['internal_order_no'], $internalValues); } }); $select = []; foreach ($itemFields as $field) { $select[] = $columns[$field] . ' AS ' . $field; } $itemsByOrder = []; foreach ($query->get($select) as $record) { $item = []; foreach ($itemFields as $field) { $value = $record->{$field} ?? null; $item[$field] = in_array($field, self::NUMERIC_FIELDS, true) ? ($value === null || $value === '' ? null : (float) $value) : $value; } $online = trim((string) ($item['online_order_no'] ?? '')); $internal = trim((string) ($item['internal_order_no'] ?? '')); $itemsByOrder[$online . "\0" . $internal][] = $item; } foreach ($rows as &$row) { $key = trim((string) $row['online_order_no']) . "\0" . trim((string) $row['internal_order_no']); $row['items'] = $itemsByOrder[$key] ?? []; } unset($row); } /** @param list $fields */ private function sumExpression(array $columns, array $fields): string { $parts = []; foreach ($fields as $field) { if (isset($columns[$field])) { $parts[] = 'COALESCE(CAST(NULLIF(TRIM(' . $this->quote($columns[$field]) . "), '') AS DECIMAL(18,2)), 0)"; } } return $parts === [] ? '0' : 'SUM(' . implode(' + ', $parts) . ')'; } private function queryDateColumn(array $columns): ?string { return Schema::connection($this->connectionName())->hasColumn( self::TABLE, GoodsProfitAfsLookupSchema::DATE_COLUMN, ) ? GoodsProfitAfsLookupSchema::DATE_COLUMN : ($columns['order_time'] ?? null); } private function connection(): ConnectionInterface { return DB::connection($this->connectionName()); } private function connectionName(): string { return (string) config('upstream_mirrors.connection', 'business_mirror'); } private function quote(string $column): string { if (preg_match('/^[A-Za-z0-9_]+$/', $column) !== 1) { throw new \InvalidArgumentException('订单利润字段名不安全。'); } return '`' . $column . '`'; } /** @param array $filters */ private function hasActiveFilters(array $filters): bool { foreach (['from', 'to', 'order_no', 'shop_name', 'sku_id'] as $key) { if (trim((string) ($filters[$key] ?? '')) !== '') { return true; } } return false; } /** @return list */ private static 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); } }