170 lines
6.1 KiB
PHP
170 lines
6.1 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Illuminate\Http\Client\PendingRequest;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\Http;
|
||
use RuntimeException;
|
||
|
||
class JushuitanOpenApiClient
|
||
{
|
||
/**
|
||
* @return array{records:list<array<string,mixed>>,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<string,mixed> $biz @return array<string,mixed> */
|
||
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<string,mixed> $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<array<string,mixed>> */
|
||
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;
|
||
}
|
||
}
|