['product_cost'], 'product_quantity' => ['product_quantity'], 'product_amount' => ['product_amount'], 'actual_shipped_quantity' => ['actual_shipped_quantity'], 'actual_shipped_amount' => ['actual_shipped_amount'], 'actual_shipped_cost' => ['actual_shipped_cost'], 'merchant_discount' => ['merchant_discount'], 'freight_income' => ['freight_income'], 'payment_amount' => ['payment_amount'], 'platform_discount' => ['platform_discount'], 'refund_amount' => ['pre_refund_amount', 'post_refund_amount'], 'platform_subsidy' => ['pre_platform_subsidy', 'post_platform_subsidy'], 'pre_platform_subsidy' => ['pre_platform_subsidy'], 'pre_application_quantity' => ['pre_application_quantity'], 'pre_application_amount' => ['pre_application_amount'], 'pre_application_cost' => ['pre_application_cost'], 'pre_return_quantity' => ['pre_return_quantity'], 'pre_return_amount' => ['pre_return_amount'], 'pre_return_cost' => ['pre_return_cost'], 'pre_refund_amount' => ['pre_refund_amount'], 'pre_refund_cost' => ['pre_refund_cost'], 'post_platform_subsidy' => ['post_platform_subsidy'], 'post_application_quantity' => ['post_application_quantity'], 'post_application_amount' => ['post_application_amount'], 'post_application_cost' => ['post_application_cost'], 'post_return_quantity' => ['post_return_quantity'], 'post_return_amount' => ['post_return_amount'], 'post_return_cost' => ['post_return_cost'], 'post_refund_amount' => ['post_refund_amount'], 'post_refund_cost' => ['post_refund_cost'], 'defective_quantity' => ['defective_quantity'], ]; private const ITEM_FIELDS = [ 'internal_order_no', 'online_order_no', 'sub_order_no', 'after_sales_no', 'order_time', 'shop_name', 'product_code', 'style_code', 'shop_product_code', 'shop_style_code', 'product_name', 'color_spec', 'product_quantity', 'product_amount', 'product_cost', 'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost', 'merchant_discount', 'freight_income', 'payment_amount', 'platform_discount', 'refund_type', 'pre_application_quantity', 'pre_application_amount', 'pre_application_cost', 'pre_return_quantity', 'pre_return_amount', 'pre_return_cost', 'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy', 'post_application_quantity', 'post_application_amount', 'post_application_cost', 'post_return_quantity', 'post_return_amount', 'post_return_cost', 'post_refund_amount', 'post_refund_cost', 'post_platform_subsidy', 'defective_quantity', 'classification_order', 'combine_type', 'shipping_warehouse', 'express_no', 'express_company', 'order_tags', 'after_sales_tags', 'item_tags', ]; private ?bool $readyCache = null; public function __construct(private readonly ?GoodsProfitAfsRepository $fields = null) { } public function ready(): bool { return $this->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']); $orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))"; $query ->selectRaw("{$shop} AS shop_name") ->selectRaw("{$online} AS online_order_no") ->selectRaw("MAX({$internal}) AS internal_order_no") ->selectRaw("{$orderKey} AS order_key") ->selectRaw("MAX({$orderTime}) AS order_time") ->selectRaw("MAX(NULLIF(TRIM({$refundType}), '')) AS refund_type") ->selectRaw("MAX(NULLIF(TRIM({$categoryOrder}), '')) AS classification_order") ->selectRaw('COUNT(*) AS source_row_count'); foreach (self::SUMMARY_NUMERIC_FIELDS as $alias => $fields) { $query->selectRaw($this->sumExpression($columns, $fields) . ' AS ' . $alias); } $records = $query ->groupByRaw("{$shop}, {$orderKey}") ->orderByRaw("MAX({$orderTime}) DESC") ->orderByDesc($columns['internal_order_no']) ->forPage($page, $limit + 1) ->get(); $rows = $records->map(static function (object $record): array { $row = [ '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_key' => trim((string) ($record->order_key ?? '')), 'order_time' => $record->order_time, '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' => [], ]; foreach (array_keys(self::SUMMARY_NUMERIC_FIELDS) as $field) { $row[$field] = round((float) ($record->{$field} ?? 0), 2); } return $row; })->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'] ?? '')); $orderKey = $online !== '' ? $online : $internal; $itemsByOrder[$orderKey][] = $item; } foreach ($rows as &$row) { $key = trim((string) ($row['order_key'] ?? '')); $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); } }