>,source_rows:int,current_page:int,has_next:bool} */ public function getCostPriceOperationHistory( Carbon $start, Carbon $end, int $page, int $pageSize, ): array { $payload = $this->request([ 'page_index' => max($page, 1), 'page_size' => min(max($pageSize, 1), 50), 'modified_begin' => $start->format('Y-m-d H:i:s'), 'modified_end' => $end->format('Y-m-d H:i:s'), ]); $records = is_array($payload['datas'] ?? null) ? $payload['datas'] : []; return [ 'records' => $this->normalizeCostPriceOperations($records), 'source_rows' => count($records), 'current_page' => max((int) ($payload['page_index'] ?? $page), 1), 'has_next' => (bool) ($payload['has_next'] ?? false), ]; } /** @param array $biz @return array */ private function request(array $biz): array { $partnerId = trim((string) config('services.jushuitan.partner_id')); $partnerKey = trim((string) config('services.jushuitan.partner_key')); $token = trim((string) config('services.jushuitan.token')); if ($partnerId === '' || $partnerKey === '' || $token === '') { throw new RuntimeException('jushuitan_classic_api_credentials_missing'); } $method = (string) config( 'services.jushuitan.product_operation_history_method', 'worklog.query', ); $timestamp = (string) now()->timestamp; // URL parameter order is part of the classic API signature contract. $params = [ 'method' => $method, 'partnerid' => $partnerId, 'token' => $token, 'ts' => $timestamp, ]; $params['sign'] = $this->sign($method, $partnerId, $token, $timestamp, $partnerKey); $baseUrl = (string) config( 'services.jushuitan.base_url', 'https://open.erp321.com/api/open/query.aspx', ); /** @var PendingRequest $request */ $request = Http::asJson() ->acceptJson() ->timeout(max((int) config('services.jushuitan.timeout', 20), 1)); $response = $request->post( $baseUrl . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986), $biz, ); 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($this->errorCode($code, $payload)); } return $payload; } /** @param array $payload */ private function errorCode(int $code, array $payload): string { $message = mb_strtolower(trim((string) ($payload['msg'] ?? $payload['message'] ?? ''))); if ($code === 100 && str_contains($message, 'partnerid')) { return 'jushuitan_classic_api_partner_id_invalid'; } return 'jushuitan_api_code_' . $code; } private function sign( string $method, string $partnerId, string $token, string $timestamp, string $partnerKey, ): string { return md5($method . $partnerId . 'token' . $token . 'ts' . $timestamp . $partnerKey); } /** @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['remark'] ?? $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['type'] ?? $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; } }