- 聚水潭奇门客户端与发货同步器骨架:QimenClient(签名+POST)、ShippingDataSynchronizer(erp.sctrade.query 列表 + erp.sradesku.query 明细 + upsert + modified 水位) - 发货明细扁平展示表 jst_oms_shipping(对齐工作平台发货数据 47 列/顺序,用于快递运费核算);原始表 jst_sctrade/_items/_skus/_receiver + jst_sync_state - 快递应付新增「发货数据」页:Pages.contentForShippingData + 视图 + business_navigation 导航项(slug=shipping-data) - ShippingOrderRepository 读展示表;artisan jushuitan:sync-shipping + 每10分钟增量调度(JST_SYNC_ENABLED 默认关) - 落库连接 jushuitan_local(本地容器,env 可覆盖);演示 Seeder(6 条家具发货样例);.env.jushuitan.example Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
131 lines
4.8 KiB
PHP
131 lines
4.8 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services\Jushuitan;
|
||
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Facades\Log;
|
||
use RuntimeException;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 聚水潭(聚石塔 / 阿里巴巴奇门)开放平台客户端。
|
||
*
|
||
* 复刻工作平台 JSCrm 的鉴权与调用方式:
|
||
* - 基础参数:app_key, customerId, format, method, simplify, timestamp, v, sign
|
||
* - 签名(奇门 MD5):MD5( secret + 依次拼接每个基础参数的 键名+键值(跳过 sign) + body + secret ),转大写十六进制
|
||
* - 发送:基础参数放 URL query,业务参数 body 作为 JSON 请求体
|
||
*
|
||
* 只读取数用途,带超时与重试。凭证来自 config('jushuitan'),缺失即拒绝请求。
|
||
*/
|
||
class QimenClient
|
||
{
|
||
/** 参与签名的基础参数顺序,必须与工作平台 BaseRequestModel 声明顺序一致。 */
|
||
private const SIGN_ORDER = ['app_key', 'customerId', 'format', 'method', 'simplify', 'timestamp', 'v'];
|
||
|
||
public function __construct(private readonly array $config)
|
||
{
|
||
}
|
||
|
||
public static function fromConfig(): self
|
||
{
|
||
return new self((array) config('jushuitan'));
|
||
}
|
||
|
||
/**
|
||
* 调用一个奇门接口,返回解析后的数组。
|
||
*
|
||
* @param string $method 接口方法名,如 erp.sctrade.query
|
||
* @param array $body 业务参数
|
||
* @return array 聚水潭返回的 JSON 解析结果
|
||
*/
|
||
public function call(string $method, array $body): array
|
||
{
|
||
$this->assertConfigured();
|
||
|
||
$bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
if ($bodyJson === false) {
|
||
throw new RuntimeException('聚水潭请求体序列化失败。');
|
||
}
|
||
|
||
$baseParams = $this->baseParams($method);
|
||
$baseParams['sign'] = $this->sign($baseParams, $bodyJson);
|
||
|
||
$sync = $this->config['sync'] ?? [];
|
||
$timeout = (int) ($sync['request_timeout'] ?? 30);
|
||
$retries = max((int) ($sync['retry_times'] ?? 5), 1);
|
||
$retrySleep = (int) ($sync['retry_sleep_ms'] ?? 1000);
|
||
|
||
$response = Http::withOptions(['query' => $baseParams])
|
||
->withHeaders(['Content-Type' => 'application/json'])
|
||
->timeout($timeout)
|
||
->retry($retries, $retrySleep)
|
||
->withBody($bodyJson, 'application/json')
|
||
->post($this->gatewayUrl());
|
||
|
||
if ($response->failed()) {
|
||
Log::warning('聚水潭奇门接口 HTTP 失败。', ['method' => $method, 'status' => $response->status()]);
|
||
throw new RuntimeException("聚水潭接口 {$method} 请求失败:HTTP {$response->status()}");
|
||
}
|
||
|
||
$data = $response->json();
|
||
if (! is_array($data)) {
|
||
throw new RuntimeException("聚水潭接口 {$method} 返回非 JSON。");
|
||
}
|
||
|
||
// 奇门通常以 code=0 / issuccess 标识成功,具体字段以实际返回为准,此处仅做基础校验。
|
||
return $data;
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function baseParams(string $method): array
|
||
{
|
||
return [
|
||
'app_key' => (string) ($this->config['app_key'] ?? ''),
|
||
'customerId' => (string) ($this->config['customer_id'] ?? ''),
|
||
'format' => (string) ($this->config['format'] ?? 'json'),
|
||
'method' => $method,
|
||
'simplify' => (string) ($this->config['simplify'] ?? 'true'),
|
||
'timestamp' => now()->format('Y-m-d H:i:s'),
|
||
'v' => (string) ($this->config['version'] ?? '2'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 奇门 MD5 签名。等价于工作平台 ErpApiHelper.SignTopRequest:
|
||
* raw = secret + Σ(key+value,按固定顺序,跳过 sign) + body + secret
|
||
*/
|
||
private function sign(array $baseParams, string $bodyJson): string
|
||
{
|
||
$secret = (string) ($this->config['app_secret'] ?? '');
|
||
$raw = $secret;
|
||
foreach (self::SIGN_ORDER as $key) {
|
||
$value = $baseParams[$key] ?? null;
|
||
if ($value !== null && $value !== '') {
|
||
$raw .= $key . $value;
|
||
}
|
||
}
|
||
$raw .= $bodyJson . $secret;
|
||
|
||
return strtoupper(md5($raw));
|
||
}
|
||
|
||
private function gatewayUrl(): string
|
||
{
|
||
$url = trim((string) ($this->config['gateway_url'] ?? ''));
|
||
if ($url === '') {
|
||
throw new RuntimeException('未配置聚水潭奇门网关地址(JST_QIMEN_URL)。');
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
private function assertConfigured(): void
|
||
{
|
||
foreach (['app_key' => 'JST_APP_KEY', 'app_secret' => 'JST_APP_SECRET', 'customer_id' => 'JST_CUSTOMER_ID'] as $key => $env) {
|
||
if (trim((string) ($this->config[$key] ?? '')) === '') {
|
||
throw new RuntimeException("未配置聚水潭凭证 {$env},已拒绝请求(不会同步任何数据)。");
|
||
}
|
||
}
|
||
}
|
||
}
|