>,source_rows:int,current_page:int,total_pages:int,total_count:int} */ public function getCostPriceOperationHistory( Carbon $start, Carbon $end, int $page, int $pageSize, ): array { $payload = $this->request([ 'current_page' => max($page, 1), 'page_size' => min(max($pageSize, 1), 2000), 'start_time' => $start->format('Y-m-d H:i:s'), 'end_time' => $end->format('Y-m-d H:i:s'), ]); $data = is_array($payload['data'] ?? null) ? $payload['data'] : []; $pageInfo = is_array($data['page'] ?? null) ? $data['page'] : []; return [ 'records' => $this->normalizeCostPriceOperations($data['list'] ?? []), 'source_rows' => is_array($data['list'] ?? null) ? count($data['list']) : 0, 'current_page' => max((int) ($pageInfo['current_page'] ?? $page), 1), 'total_pages' => max((int) ($pageInfo['pages'] ?? 0), 0), 'total_count' => max((int) ($pageInfo['count'] ?? 0), 0), ]; } /** @param array $biz @return array */ private function request(array $biz): array { $appKey = trim((string) config('services.jushuitan.app_key')); $appSecret = trim((string) config('services.jushuitan.app_secret')); $accessToken = trim((string) config('services.jushuitan.access_token')); if ($appKey === '' || $appSecret === '' || $accessToken === '') { throw new RuntimeException('jushuitan_api_credentials_missing'); } $params = [ 'access_token' => $accessToken, 'app_key' => $appKey, 'biz' => json_encode( $biz, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR, ), 'charset' => 'utf-8', 'timestamp' => (string) now()->timestamp, 'version' => '2', ]; $params['sign'] = $this->sign($params, $appSecret); $baseUrl = rtrim((string) config('services.jushuitan.base_url'), '/'); $path = (string) config( 'services.jushuitan.product_operation_history_path', '/open/webapi/itemapi/itemsku/queryitemskuopshistory', ); /** @var PendingRequest $request */ $request = Http::asForm() ->acceptJson() ->timeout(max((int) config('services.jushuitan.timeout', 20), 1)); $response = $request->post($baseUrl . '/' . ltrim($path, '/'), $params); if (! $response->successful()) { throw new RuntimeException('jushuitan_api_http_' . $response->status()); } $payload = $response->json(); if (! is_array($payload)) { throw new RuntimeException('jushuitan_api_invalid_json'); } $code = (int) ($payload['code'] ?? -1); if ($code !== 0) { throw new RuntimeException('jushuitan_api_code_' . $code); } return $payload; } /** @param array $params */ private function sign(array $params, string $appSecret): string { ksort($params, SORT_STRING); $plain = $appSecret; foreach ($params as $key => $value) { if ($key === 'sign' || $value === null || $value === '') { continue; } $plain .= $key . (string) $value; } return md5($plain); } /** @param mixed $records @return list> */ private function normalizeCostPriceOperations(mixed $records): array { if (! is_array($records)) { return []; } $result = []; foreach ($records as $record) { if (! is_array($record)) { continue; } $skuId = trim((string) ($record['sku_id'] ?? '')); $description = trim((string) ($record['operation_description'] ?? '')); if ($skuId === '' || ! str_contains($description, '采购价')) { continue; } [$beforePrice, $afterPrice] = $this->pricesFromDescription($description); $result[] = [ 'operation_id' => (string) ($record['autoid'] ?? ''), 'sku_id' => $skuId, 'i_id' => $this->nullableString($record['i_id'] ?? null), 'product_name' => $this->nullableString($record['name'] ?? null), 'properties_value' => $this->nullableString($record['properties_value'] ?? null), 'sku_type' => $this->nullableString($record['sku_type'] ?? null), 'operation_type' => $this->nullableString($record['operation_type'] ?? null), 'operation_description' => $description, 'operator_name' => $this->nullableString($record['creator_name'] ?? null), 'operated_at' => $this->nullableString($record['created'] ?? null), 'before_price' => $beforePrice, 'after_price' => $afterPrice, 'diff_amount' => ($beforePrice === null || $afterPrice === null) ? null : $afterPrice - $beforePrice, ]; } return $result; } /** @return array{0:?float,1:?float} */ private function pricesFromDescription(string $description): array { $matched = preg_match( '/采购价\s*[::]\s*(-?\d+(?:\.\d+)?)\s*(?:->|=>|→|至)\s*(-?\d+(?:\.\d+)?)/u', $description, $matches, ); if ($matched !== 1) { return [null, null]; } return [(float) $matches[1], (float) $matches[2]]; } private function nullableString(mixed $value): ?string { if ($value === null) { return null; } $value = trim((string) $value); return $value === '' ? null : $value; } }