available === null) { try { $this->available = Schema::hasTable(self::TABLE) && DB::table(self::TABLE)->exists(); } catch (\Throwable) { $this->available = false; } } return $this->available; } /** * 导入数据是否覆盖本次筛选(有至少一行命中即视为覆盖)。 * * @param array $filters */ public function covers(array $filters): bool { if (! $this->available()) { return false; } // 指定单号时,导入表里命中该单就完整(同单的所有明细行都在导入数据里)。 if (trim((string) ($filters['order_no'] ?? '')) !== '') { return $this->filtered($filters)->limit(1)->exists(); } $from = trim((string) ($filters['from'] ?? '')); $to = trim((string) ($filters['to'] ?? '')); // 不限日期时无法保证覆盖范围,回退镜像,避免把导入数据当成全量。 if ($from === '' || $to === '') { return false; } $range = $this->filtered(['shop_names' => $filters['shop_names'] ?? []]) ->selectRaw('MIN(order_time) as min_time, MAX(order_time) as max_time') ->first(); if (! $range || $range->min_time === null) { return false; } // 只有导入范围完整覆盖所选日期时才使用导入数据,避免跨月时数据不全。 return substr((string) $range->min_time, 0, 10) <= $from && substr((string) $range->max_time, 0, 10) >= $to; } /** * @param array $filters * @return array{rows:list>,has_more:bool} */ public function paginate(array $filters, int $page, int $limit): array { $page = max($page, 1); $limit = min(max($limit, 1), 100); $records = $this->filtered($filters) ->orderByDesc('order_time') ->orderByDesc('id') ->forPage($page, $limit + 1) ->get(); $rows = []; foreach ($records as $record) { $payload = json_decode((string) $record->payload, true); $rows[] = is_array($payload) ? $payload : []; } return [ 'rows' => array_slice($rows, 0, $limit), 'has_more' => count($rows) > $limit, ]; } /** * 商品成本补拆:按线上订单号批量读取导入表中已经拆好的组件行。 * * 只返回拆分所需的商品、平台规格、数量和金额;单价仍由商品成本的 * 月加权平均价 / 线上代发价 / 普通商品资料成本价链路计算。 * * @param list $onlineOrderNos * @return list */ public function componentRowsForOrders(array $onlineOrderNos): array { $onlineOrderNos = array_values(array_unique(array_filter(array_map( static fn (mixed $value): string => trim((string) $value), $onlineOrderNos, ), static fn (string $value): bool => $value !== ''))); sort($onlineOrderNos, SORT_STRING); if ($onlineOrderNos === [] || ! $this->available()) { return []; } $rows = []; foreach (array_chunk($onlineOrderNos, 500) as $chunk) { foreach (DB::table(self::TABLE) ->whereIn('online_order_no', $chunk) ->orderBy('id') ->get([ 'shop_name', 'online_order_no', 'internal_order_no', 'product_code', 'product_quantity', 'product_amount', 'payload', ]) as $record) { $payload = json_decode((string) ($record->payload ?? ''), true); $payload = is_array($payload) ? $payload : []; $productCode = mb_strtoupper(trim((string) ( $record->product_code ?? ($payload['product_code'] ?? '') ))); if ($productCode === '') { continue; } $rows[] = [ '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 ?? '')), 'product_code' => $productCode, 'shop_product_code' => trim((string) ($payload['shop_product_code'] ?? '')), 'shop_style_code' => trim((string) ($payload['shop_style_code'] ?? '')), 'product_quantity' => (float) ($record->product_quantity ?? 0), 'product_amount' => (float) ($record->product_amount ?? 0), ]; } } return $rows; } /** * 导入一行(命令内部使用,批量写入)。 * * @param array $payload */ public static function toRecord(array $payload, ?string $importedAt = null): array { // 以整行内容做键:同一单同一商品可能出现多行,只有完全相同的一行才合并。 ksort($payload); $rowKey = sha1((string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $now = $importedAt ?? now()->toDateTimeString(); return [ 'row_key' => $rowKey, 'shop_name' => mb_substr((string) ($payload['shop_name'] ?? ''), 0, 191), 'shop_code' => mb_substr((string) ($payload['shop_code'] ?? ''), 0, 64), 'online_order_no' => mb_substr((string) ($payload['online_order_no'] ?? ''), 0, 64), 'internal_order_no' => mb_substr((string) ($payload['internal_order_no'] ?? ''), 0, 64), 'product_code' => mb_substr((string) ($payload['product_code'] ?? ''), 0, 191), 'order_time' => self::dateTime($payload['order_time'] ?? null), 'product_quantity' => (float) ($payload['product_quantity'] ?? 0), 'product_amount' => (float) ($payload['product_amount'] ?? 0), 'product_cost' => (float) ($payload['product_cost'] ?? 0), 'cost_source' => mb_substr((string) ($payload['cost_source'] ?? ''), 0, 32), 'combine_type' => mb_substr((string) ($payload['combine_type'] ?? ''), 0, 32), 'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'imported_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; } /** @param array $filters */ private function filtered(array $filters): Builder { $query = DB::table(self::TABLE); $from = trim((string) ($filters['from'] ?? '')); $to = trim((string) ($filters['to'] ?? '')); $orderNo = trim((string) ($filters['order_no'] ?? '')); $sku = trim((string) ($filters['sku_id'] ?? '')); $shops = array_values(array_filter((array) ($filters['shop_names'] ?? []), static fn ($v) => trim((string) $v) !== '')); if ($from !== '') { $query->where('order_time', '>=', $from . ' 00:00:00'); } if ($to !== '') { $query->where('order_time', '<=', $to . ' 23:59:59'); } if ($shops !== []) { $query->whereIn('shop_name', $shops); } if ($orderNo !== '') { $query->where(function (Builder $inner) use ($orderNo): void { $inner->where('online_order_no', 'like', $orderNo . '%') ->orWhere('internal_order_no', 'like', $orderNo . '%') ->orWhere('internal_order_no', '=', $orderNo); }); } if ($sku !== '') { $query->where('product_code', 'like', '%' . addcslashes($sku, '%_\\') . '%'); } return $query; } private static function dateTime(mixed $value): ?string { $value = trim((string) $value); if ($value === '') { return null; } $timestamp = strtotime($value); return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp); } }