178 lines
6.2 KiB
PHP
178 lines
6.2 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,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<string,mixed> $biz @return array<string,mixed> */
|
||
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<string,mixed> $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<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['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;
|
||
}
|
||
}
|