limeng b038909cb0 feat: 发货数据页接入(caiwu 直连聚水潭/奇门)
- 聚水潭奇门客户端与发货同步器骨架: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
2026-09-16 10:57:12 +08:00

131 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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},已拒绝请求(不会同步任何数据)。");
}
}
}
}