更新
This commit is contained in:
@@ -403,6 +403,11 @@ export function prescriptionOrderLogisticsTrace(params: {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/logisticsTrace', params })
|
||||
}
|
||||
|
||||
/** 直接调用京东官方物流接口刷新轨迹并落库(绕过快递100缓存) */
|
||||
export function prescriptionOrderLogisticsJdUpdate(params: { id: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/logisticsJdUpdate', params })
|
||||
}
|
||||
|
||||
export function prescriptionOrderDetail(params: { id: number }) {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/detail', params })
|
||||
}
|
||||
|
||||
@@ -1353,6 +1353,17 @@
|
||||
<template #icon><el-icon><Refresh /></el-icon></template>
|
||||
刷新轨迹
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.prescriptionOrder/logisticsTrace']"
|
||||
type="warning"
|
||||
plain
|
||||
size="default"
|
||||
:loading="jdLogisticsUpdating"
|
||||
@click="updateJdLogistics"
|
||||
>
|
||||
<template #icon><el-icon><Van /></el-icon></template>
|
||||
京东接口更新
|
||||
</el-button>
|
||||
<div class="ml-2 flex items-center gap-4 border-l pl-4 border-gray-200">
|
||||
<el-link :href="detailOfficialUrls.sf" target="_blank" type="primary" :underline="false" class="text-[13px]">顺丰官网</el-link>
|
||||
<el-link :href="detailOfficialUrls.jd" target="_blank" type="primary" :underline="false" class="text-[13px]">京东物流</el-link>
|
||||
@@ -2870,7 +2881,7 @@
|
||||
<script lang="ts" setup name="prescriptionOrderList">
|
||||
import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, Search, Calendar, Document, Link as LinkIcon, Wallet, Van } from '@element-plus/icons-vue'
|
||||
import ListTimeFilter from '@/components/list-time-filter/index.vue'
|
||||
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
|
||||
import {
|
||||
@@ -2883,6 +2894,7 @@ import {
|
||||
prescriptionOrderPaidPayOrders,
|
||||
prescriptionOrderWithdraw,
|
||||
prescriptionOrderLogisticsTrace,
|
||||
prescriptionOrderLogisticsJdUpdate,
|
||||
prescriptionOrderShip,
|
||||
prescriptionOrderLogs,
|
||||
prescriptionOrderUpdateAmount,
|
||||
@@ -4181,6 +4193,7 @@ function formatOrderTime(v: unknown) {
|
||||
|
||||
const detailLogisticsExpress = ref<string>('auto')
|
||||
const logisticsTraceLoading = ref(false)
|
||||
const jdLogisticsUpdating = ref(false)
|
||||
const logisticsTracePayload = ref<Record<string, any> | null>(null)
|
||||
/** 顺丰/快递100:与运单一致的收件手机后四位(可覆盖订单收货手机) */
|
||||
const logisticsTracePhoneTail = ref('')
|
||||
@@ -4414,6 +4427,26 @@ async function fetchLogisticsTrace() {
|
||||
}
|
||||
}
|
||||
|
||||
// 直接请求京东官方物流接口,刷新并落库轨迹后重新读取展示
|
||||
async function updateJdLogistics() {
|
||||
const orderId = Number(detailData.value?.id)
|
||||
const trackingNumber = String(detailData.value?.tracking_number || '').trim()
|
||||
if (!orderId || !trackingNumber) {
|
||||
feedback.msgWarning('当前订单未填写快递单号')
|
||||
return
|
||||
}
|
||||
jdLogisticsUpdating.value = true
|
||||
try {
|
||||
const res: any = await prescriptionOrderLogisticsJdUpdate({ id: orderId })
|
||||
feedback.msgSuccess(res?.message || '京东物流轨迹已更新')
|
||||
await fetchLogisticsTrace()
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || '京东物流更新失败')
|
||||
} finally {
|
||||
jdLogisticsUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const patchRxPatientVisible = ref(false)
|
||||
const patchRxPatientSaving = ref(false)
|
||||
const patchRxPatientFormRef = ref<FormInstance>()
|
||||
|
||||
@@ -102,6 +102,24 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
return $this->success('', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接调用京东官方物流接口刷新轨迹并落库(绕过快递100缓存)
|
||||
*/
|
||||
public function logisticsJdUpdate()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('logisticsJdUpdate');
|
||||
$data = PrescriptionOrderLogic::logisticsJdUpdate(
|
||||
(int) $params['id'],
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
);
|
||||
if ($data === null) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success((string) ($data['message'] ?? '京东物流轨迹已更新'), $data);
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('edit');
|
||||
|
||||
@@ -126,6 +126,12 @@ class AuthMiddleware
|
||||
return true;
|
||||
}
|
||||
|
||||
// 京东物流接口刷新:与「物流轨迹」同一数据域,复用 logisticsTrace 权限
|
||||
if ($accessUri === 'tcm.prescriptionorder/logisticsjdupdate'
|
||||
&& in_array('tcm.prescriptionorder/logisticstrace', $adminUris, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 挂号列表批量改渠道:与单条编辑同一数据域,复用 doctor.appointment/edit
|
||||
if ($accessUri === 'doctor.appointment/batcheditchannel'
|
||||
&& in_array('doctor.appointment/edit', $adminUris, true)) {
|
||||
|
||||
@@ -1525,6 +1525,48 @@ class PrescriptionOrderLogic
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接调用京东官方物流接口刷新轨迹并落库(后台「京东接口更新」按钮)。
|
||||
*
|
||||
* @return array{success:bool, message:string, traces_count:int, state:string}|null
|
||||
*/
|
||||
public static function logisticsJdUpdate(int $id, int $adminId, array $adminInfo): ?array
|
||||
{
|
||||
self::$error = '';
|
||||
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$row) {
|
||||
self::$error = '订单不存在';
|
||||
|
||||
return null;
|
||||
}
|
||||
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限操作';
|
||||
|
||||
return null;
|
||||
}
|
||||
$num = trim((string) ($row->tracking_number ?? ''));
|
||||
if ($num === '') {
|
||||
self::$error = '未填写快递单号';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = \app\common\service\ExpressTrackingService::queryJdOfficialAndUpdate($num);
|
||||
|
||||
if (empty($ret['success'])) {
|
||||
self::$error = (string) ($ret['message'] ?? '京东物流更新失败');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => (string) ($ret['message'] ?? '京东物流轨迹已更新'),
|
||||
'traces_count' => (int) ($ret['traces_count'] ?? 0),
|
||||
'state' => (string) ($ret['state'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
* @return array<string,mixed>|false
|
||||
|
||||
@@ -68,6 +68,7 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'remark_assistant', 'pay_order_ids',
|
||||
],
|
||||
'logisticsTrace' => ['id', 'phone_tail'],
|
||||
'logisticsJdUpdate' => ['id'],
|
||||
'auditPrescription' => ['id', 'action', 'remark'],
|
||||
'auditPayment' => ['id', 'action', 'remark'],
|
||||
'withdraw' => ['id'],
|
||||
|
||||
@@ -88,7 +88,8 @@ class ExpressTrackService
|
||||
// 未配置京东官方接口时 isConfigured()=false,本段跳过,行为与原先一致。
|
||||
if ($carrier === 'jd' && JdLogisticsService::isConfigured()) {
|
||||
try {
|
||||
$jd = JdLogisticsService::queryTrace($num);
|
||||
$jdPhone = $overrideDigits !== '' ? $overrideDigits : $recipientDigits;
|
||||
$jd = JdLogisticsService::queryTrace($num, $jdPhone);
|
||||
if ($jd !== null && !empty($jd['traces']) && self::jdResultPreferred($jd, $result)) {
|
||||
$result['traces'] = $jd['traces'];
|
||||
$result['state'] = (string) $jd['state'];
|
||||
|
||||
@@ -353,6 +353,93 @@ class ExpressTrackingService
|
||||
|
||||
$responseTime = (int) ((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return self::persistTrackingResult($tracking, $result, $isAuto, $responseTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接调用京东官方物流接口(绕过快递100),将轨迹落库到 express_tracking / express_trace。
|
||||
* 供后台「京东接口更新」按钮使用;未配置京东官方接口或无轨迹时返回失败。
|
||||
*
|
||||
* @return array{success:bool, message:string, traces_count:int, state:string, source:string}
|
||||
*/
|
||||
public static function queryJdOfficialAndUpdate(string $trackingNumber): array
|
||||
{
|
||||
$fail = static fn (string $msg): array => [
|
||||
'success' => false,
|
||||
'message' => $msg,
|
||||
'traces_count' => 0,
|
||||
'state' => '',
|
||||
'source' => 'jd_official',
|
||||
];
|
||||
|
||||
$tn = trim($trackingNumber);
|
||||
if ($tn === '') {
|
||||
return $fail('快递单号不能为空');
|
||||
}
|
||||
if (!JdLogisticsService::isConfigured()) {
|
||||
return $fail('未配置京东官方物流接口(请在 .env 配置 JD_LOGISTICS_* 后重试)');
|
||||
}
|
||||
|
||||
$tracking = self::resolveTrackingByNumber($tn);
|
||||
if (!$tracking) {
|
||||
return $fail('未找到该快递单号对应的业务订单或物流追踪记录');
|
||||
}
|
||||
|
||||
$startTime = microtime(true);
|
||||
try {
|
||||
$jd = JdLogisticsService::queryTrace($tn, (string) ($tracking->recipient_phone ?? ''));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('queryJdOfficialAndUpdate failed', [
|
||||
'tracking_number' => $tn,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $fail('京东官方接口请求异常:' . $e->getMessage());
|
||||
}
|
||||
$responseTime = (int) ((microtime(true) - $startTime) * 1000);
|
||||
|
||||
if ($jd === null || empty($jd['traces'])) {
|
||||
self::logQuery($tn, (string) $tracking->express_company, 'manual', [
|
||||
'source' => 'jd_official',
|
||||
'hint' => '京东官方接口未返回有效轨迹',
|
||||
'traces' => [],
|
||||
], $responseTime);
|
||||
|
||||
return $fail('京东官方接口未返回有效轨迹,请确认运单号或稍后重试');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'carrier' => 'jd',
|
||||
'carrier_label' => '京东快递',
|
||||
'kuaidi_com' => 'jd',
|
||||
'traces' => $jd['traces'],
|
||||
'state' => (string) ($jd['state'] ?? '0'),
|
||||
'state_text' => (string) ($jd['state_text'] ?? ''),
|
||||
'source' => 'jd_official',
|
||||
'hint' => '',
|
||||
'official_url' => '',
|
||||
];
|
||||
|
||||
self::persistTrackingResult($tracking, $result, false, $responseTime);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '京东物流轨迹已更新',
|
||||
'traces_count' => count($jd['traces']),
|
||||
'state' => (string) ($jd['state_text'] ?? ''),
|
||||
'source' => 'jd_official',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一次轨迹查询结果(快递100 / 京东官方等)落库并推进订单状态。
|
||||
*
|
||||
* @param array<string, mixed> $result query 返回的轨迹结构
|
||||
* @param bool $isAuto 是否定时任务触发(影响查询日志类型与医助释放)
|
||||
* @return array<string, mixed>|null 查询结果;自动任务时可能含 _assistant_sync
|
||||
*/
|
||||
private static function persistTrackingResult(ExpressTracking $tracking, array $result, bool $isAuto, int $responseTime): ?array
|
||||
{
|
||||
// 记录查询日志
|
||||
self::logQuery(
|
||||
$tracking->tracking_number,
|
||||
|
||||
@@ -8,36 +8,42 @@ use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 京东官方物流轨迹查询(JOS 开放平台 https://api.jd.com/routerjson)
|
||||
* 京东官方物流轨迹查询(京东物流开放平台 LOP,https://api.jdl.com)
|
||||
*
|
||||
* 作用:作为快递100 对「京东自营运单(JDVE…)」轨迹陈旧/缺失时的兜底数据源。
|
||||
* 仅在 config/logistics.php 的 jd.enable=true(即填好 app_key/app_secret/access_token)时生效;
|
||||
* 作用:作为快递100 对「京东自营运单(JDVE…)」轨迹陈旧/缺失时的兜底数据源,
|
||||
* 同时供后台「京东接口更新」按钮直接拉取并落库。
|
||||
* 仅在 config/logistics.php 的 jd.enable=true(填好 app_key/app_secret/access_token)时生效;
|
||||
* 未配置时 isConfigured()=false,ExpressTrackService 完全沿用快递100 逻辑,互不影响。
|
||||
*
|
||||
* 默认调用青龙「运单轨迹查询」jingdong.ldop.receive.trace.get;
|
||||
* 若你方用 ECLP 等其它接口,可在 .env 覆盖 JD_LOGISTICS_METHOD / JD_LOGISTICS_PARAM_KEY / JD_LOGISTICS_WAYBILL_FIELD,
|
||||
* 无需改代码(响应解析采用「递归找轨迹行」的宽松策略,兼容多种返回结构)。
|
||||
* 接口:京东物流标准轨迹服务 /jd/tracking/query(2025-04-29 改版,对接方案编码 Tracking_JD)。
|
||||
* 调用走 LOP 统一网关,鉴权/签名规则与官方 SDK(IsvFilter) 完全一致:
|
||||
* - 公共参数(app_key/access_token/timestamp/v/sign/algorithm/LOP-DN)以 query string 拼到 URL;
|
||||
* - 业务参数 JSON 字符串作为请求体;待签串固定顺序拼接并首尾包 app_secret。
|
||||
* 加签算法由 .env JD_LOGISTICS_ALGORITHM 控制(默认 md5-salt=md5(content),另支持 HMacMD5/SHA1/SHA256/SHA512)。
|
||||
* 注意:后台「报文加解密密钥」的 RSA 公私钥仅用于报文加解密,与本网关签名无关。
|
||||
*
|
||||
* 签名(JOS 标准):sign = strtoupper(md5(secret + 拼接(ksort 后的 key.value) + secret))
|
||||
* 网关/path/对接方案编码/单号类型 走 .env(JD_LOGISTICS_GATEWAY / METHOD / LOP_DN / REFERENCE_TYPE)。
|
||||
* 响应解析采用「递归找轨迹行」的宽松策略,兼容多种返回结构。
|
||||
*/
|
||||
final class JdLogisticsService
|
||||
{
|
||||
/** 轨迹行「时间」候选字段(按优先级) */
|
||||
private const TIME_FIELDS = [
|
||||
'operatorTime', 'opeTime', 'operateTime', 'msgTime', 'scanTime',
|
||||
'operationTime', 'operatorTime', 'opeTime', 'operateTime', 'msgTime', 'scanTime',
|
||||
'time', 'createTime', 'waybillStateTime', 'orderTime',
|
||||
];
|
||||
|
||||
/** 轨迹行「描述」候选字段(按优先级) */
|
||||
private const CONTEXT_FIELDS = [
|
||||
'operateRemark', 'opeRemark', 'content', 'opeTitle', 'remark',
|
||||
'scanTypeName', 'waybillStateName', 'message', 'desc', 'msg',
|
||||
'remark', 'operateRemark', 'opeRemark', 'content', 'opeTitle',
|
||||
'operationCodeName', 'operationTypeName', 'scanTypeName', 'waybillStateName', 'message', 'desc', 'msg',
|
||||
];
|
||||
|
||||
public static function isConfigured(): bool
|
||||
{
|
||||
$cfg = Config::get('logistics.jd', []);
|
||||
|
||||
// LOP 网关签名用 app_secret(md5-salt / HMAC),不需要 RSA 私钥(私钥仅用于报文加解密)
|
||||
return !empty($cfg['enable'])
|
||||
&& trim((string) ($cfg['app_key'] ?? '')) !== ''
|
||||
&& trim((string) ($cfg['app_secret'] ?? '')) !== ''
|
||||
@@ -56,7 +62,7 @@ final class JdLogisticsService
|
||||
* newest_unix: int
|
||||
* }|null
|
||||
*/
|
||||
public static function queryTrace(string $waybillCode): ?array
|
||||
public static function queryTrace(string $waybillCode, string $phoneTail = ''): ?array
|
||||
{
|
||||
$num = trim($waybillCode);
|
||||
if ($num === '' || !self::isConfigured()) {
|
||||
@@ -64,15 +70,23 @@ final class JdLogisticsService
|
||||
}
|
||||
|
||||
$cfg = Config::get('logistics.jd', []);
|
||||
$bizParam = [
|
||||
(string) ($cfg['waybill_field'] ?? 'waybillCode') => $num,
|
||||
|
||||
// 京东物流标准轨迹服务 /jd/tracking/query:body 为 JSON 数组 [{referenceNumber, referenceType, phone}]
|
||||
$row = [
|
||||
(string) ($cfg['reference_field'] ?? 'referenceNumber') => $num,
|
||||
'referenceType' => (string) ($cfg['reference_type'] ?? '20000'),
|
||||
];
|
||||
$tail = substr(preg_replace('/\D/', '', $phoneTail) ?? '', -4);
|
||||
if ($tail !== '') {
|
||||
$row['phone'] = $tail;
|
||||
}
|
||||
$customerCode = trim((string) ($cfg['customer_code'] ?? ''));
|
||||
if ($customerCode !== '') {
|
||||
$bizParam['customerCode'] = $customerCode;
|
||||
$row['customerCode'] = $customerCode;
|
||||
}
|
||||
$body = json_encode([$row], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$raw = self::request($cfg, $bizParam);
|
||||
$raw = self::request($cfg, (string) $body);
|
||||
if ($raw === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -84,7 +98,19 @@ final class JdLogisticsService
|
||||
return null;
|
||||
}
|
||||
|
||||
// JOS 网关层错误(如 access_token 失效)在顶层返回 error_response
|
||||
// LOP 网关/业务错误:code 非 1000(成功)时记录原始报文,便于排查鉴权/单号/权限问题
|
||||
$code = (string) ($json['code'] ?? $json['resultCode'] ?? '');
|
||||
if ($code !== '' && !in_array($code, ['1000', '0000', '0'], true)) {
|
||||
Log::warning('JdLogisticsService lop error', [
|
||||
'num' => $num,
|
||||
'code' => $code,
|
||||
'message' => (string) ($json['msg'] ?? $json['message'] ?? $json['resultMessage'] ?? ''),
|
||||
'raw' => mb_substr($raw, 0, 500),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
// 兼容 JOS 网关层错误结构
|
||||
if (isset($json['error_response'])) {
|
||||
Log::warning('JdLogisticsService gateway error', [
|
||||
'num' => $num,
|
||||
@@ -142,43 +168,90 @@ final class JdLogisticsService
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 LOP 网关(统一鉴权/签名,与官方 SDK IsvFilter 一致)。
|
||||
*
|
||||
* 公共参数以 query string 拼到 URL;业务参数(JSON 字符串)作为请求体;
|
||||
* 网关靠 LOP-DN(对接方案编码) 路由到对应服务。
|
||||
*
|
||||
* @param array<string,mixed> $cfg
|
||||
* @param array<string,mixed> $bizParam
|
||||
* @param string $body 业务参数 JSON 字符串(param_json)
|
||||
*/
|
||||
private static function request(array $cfg, array $bizParam): ?string
|
||||
private static function request(array $cfg, string $body): ?string
|
||||
{
|
||||
$paramKey = (string) ($cfg['param_key'] ?? '360buy_param_json');
|
||||
$params = [
|
||||
'method' => (string) ($cfg['method'] ?? 'jingdong.ldop.receive.trace.get'),
|
||||
'app_key' => (string) ($cfg['app_key'] ?? ''),
|
||||
'access_token' => (string) ($cfg['access_token'] ?? ''),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'format' => 'json',
|
||||
'v' => (string) ($cfg['api_version'] ?? '2.0'),
|
||||
$paramKey => json_encode($bizParam, JSON_UNESCAPED_UNICODE),
|
||||
$appKey = (string) ($cfg['app_key'] ?? '');
|
||||
$appSecret = (string) ($cfg['app_secret'] ?? '');
|
||||
$accessToken = (string) ($cfg['access_token'] ?? '');
|
||||
$path = (string) ($cfg['method'] ?? '/jd/tracking/query');
|
||||
$version = (string) ($cfg['api_version'] ?? '2.0');
|
||||
$algorithm = trim((string) ($cfg['algorithm'] ?? 'md5-salt')) ?: 'md5-salt';
|
||||
$lopDn = (string) ($cfg['lop_dn'] ?? 'Tracking_JD');
|
||||
// 时间戳与时区必须自洽(否则网关报 471 时间戳已失效):统一用北京时间 + lop-tz=8
|
||||
$now = new \DateTime('now', new \DateTimeZone('Asia/Shanghai'));
|
||||
$timestamp = $now->format('Y-m-d H:i:s');
|
||||
|
||||
// 待签串:固定顺序拼接,首尾包 appSecret(method=接口path,param_json=业务体)
|
||||
$content = implode('', [
|
||||
$appSecret,
|
||||
'access_token', $accessToken,
|
||||
'app_key', $appKey,
|
||||
'method', $path,
|
||||
'param_json', $body,
|
||||
'timestamp', $timestamp,
|
||||
'v', $version,
|
||||
$appSecret,
|
||||
]);
|
||||
$sign = self::sign($algorithm, $content, $appSecret);
|
||||
if ($sign === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = [
|
||||
'LOP-DN' => $lopDn,
|
||||
'app_key' => $appKey,
|
||||
'access_token' => $accessToken,
|
||||
'timestamp' => $timestamp,
|
||||
'v' => $version,
|
||||
'sign' => $sign,
|
||||
'algorithm' => $algorithm,
|
||||
];
|
||||
|
||||
$params['sign'] = self::sign($params, (string) ($cfg['app_secret'] ?? ''));
|
||||
$base = rtrim((string) ($cfg['gateway'] ?? 'https://api.jdl.com'), '/');
|
||||
$url = $base . $path . '?' . http_build_query($query);
|
||||
|
||||
$gateway = (string) ($cfg['gateway'] ?? 'https://api.jd.com/routerjson');
|
||||
// lop-tz:与 timestamp 同源(北京时间 = 东八区 = 8)
|
||||
$offsetHours = (int) ($now->getOffset() / 3600);
|
||||
|
||||
return self::httpPostForm($gateway, http_build_query($params));
|
||||
return self::httpPostJson($url, $body, [
|
||||
'Content-Type: application/json;charset=utf-8',
|
||||
'User-Agent: lop-http/php',
|
||||
'lop-tz: ' . $offsetHours,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* JOS 签名:strtoupper(md5(secret + 拼接(ksort 后的 key.value) + secret))
|
||||
*
|
||||
* @param array<string,string> $params
|
||||
* LOP 网关签名(与官方 SDK Utils::sign 一致):
|
||||
* - md5-salt :md5(content) 的小写十六进制
|
||||
* - HMacMD5 / HMacSHA1 / HMacSHA256 / HMacSHA512:base64(hmac(算法, content, appSecret))
|
||||
* 不支持的算法返回 null。
|
||||
*/
|
||||
private static function sign(array $params, string $secret): string
|
||||
private static function sign(string $algorithm, string $content, string $secret): ?string
|
||||
{
|
||||
ksort($params);
|
||||
$concat = '';
|
||||
foreach ($params as $k => $v) {
|
||||
$concat .= $k . $v;
|
||||
}
|
||||
switch (trim($algorithm)) {
|
||||
case 'md5-salt':
|
||||
return md5($content);
|
||||
case 'HMacMD5':
|
||||
return base64_encode(hash_hmac('md5', $content, $secret, true));
|
||||
case 'HMacSHA1':
|
||||
return base64_encode(hash_hmac('sha1', $content, $secret, true));
|
||||
case 'HMacSHA256':
|
||||
return base64_encode(hash_hmac('sha256', $content, $secret, true));
|
||||
case 'HMacSHA512':
|
||||
return base64_encode(hash_hmac('sha512', $content, $secret, true));
|
||||
default:
|
||||
Log::warning('JdLogisticsService unsupported algorithm', ['algorithm' => $algorithm]);
|
||||
|
||||
return strtoupper(md5($secret . $concat . $secret));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +429,10 @@ final class JdLogisticsService
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function httpPostForm(string $url, string $body): ?string
|
||||
/**
|
||||
* @param list<string> $headers
|
||||
*/
|
||||
private static function httpPostJson(string $url, string $body, array $headers): ?string
|
||||
{
|
||||
if (!function_exists('curl_init')) {
|
||||
return null;
|
||||
@@ -367,9 +443,7 @@ final class JdLogisticsService
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$resp = curl_exec($ch);
|
||||
|
||||
@@ -41,9 +41,11 @@ $forceDisable = filter_var($forceDisableRaw, FILTER_VALIDATE_BOOLEAN);
|
||||
// 已填 CUSTOMER+KEY 即启用;需临时关闭时设 LOGISTICS_KUAIDI100_DISABLE=true(LOGISTICS_KUAIDI100_ENABLE 已废弃,可删)
|
||||
$kuaidiEnabled = ! $forceDisable && $hasCreds;
|
||||
|
||||
// 京东官方物流接口(JOS 开放平台 https://api.jd.com/routerjson)
|
||||
// 仅当填好 APP_KEY / APP_SECRET / ACCESS_TOKEN 时启用;作为快递100 对京东自营单(JDVE…)轨迹陈旧时的兜底数据源。
|
||||
// 京东物流开放平台 LOP(https://api.jdl.com)
|
||||
// 仅当填好 APP_KEY / APP_SECRET / ACCESS_TOKEN(RSA2 模式还需 RSA_PRIVATE_KEY)时启用;
|
||||
// 作为快递100 对京东自营单(JDVE…)轨迹陈旧时的兜底数据源,并供后台「京东接口更新」按钮使用。
|
||||
// 未配置时本段 enable=false,ExpressTrackService 完全沿用现有快递100 逻辑,互不影响。
|
||||
// 加签方式由 JD_LOGISTICS_ALGORITHM 控制:rsa2(默认,需私钥) / md5(摘要,无需私钥)。
|
||||
$envJd = static function (string $suffix, $default = '') use ($stripQuotes) {
|
||||
$top = 'JD_LOGISTICS_' . $suffix;
|
||||
$v = env($top, null);
|
||||
@@ -79,12 +81,17 @@ return [
|
||||
'app_key' => $jdAppKey,
|
||||
'app_secret' => $jdAppSecret,
|
||||
'access_token' => $jdAccessToken,
|
||||
'gateway' => $envJd('GATEWAY', 'https://api.jd.com/routerjson'),
|
||||
// 青龙运单轨迹查询;如改用 ECLP 等可在 .env 覆盖 method / param_key / waybill_field
|
||||
'method' => $envJd('METHOD', 'jingdong.ldop.receive.trace.get'),
|
||||
'param_key' => $envJd('PARAM_KEY', '360buy_param_json'),
|
||||
'waybill_field' => $envJd('WAYBILL_FIELD', 'waybillCode'),
|
||||
// 部分接口需商家编码 / 事业部编码,可选
|
||||
// LOP 网关签名算法(与官方 SDK 一致):md5-salt(默认) / HMacMD5 / HMacSHA1 / HMacSHA256 / HMacSHA512
|
||||
'algorithm' => $envJd('ALGORITHM', 'md5-salt'),
|
||||
// 生产网关基址(不含路径)
|
||||
'gateway' => $envJd('GATEWAY', 'https://api.jdl.com'),
|
||||
// 接口 path(即签名里的 method);京东物流标准轨迹服务(2025-04-29 改版)
|
||||
'method' => $envJd('METHOD', '/jd/tracking/query'),
|
||||
// LOP-DN:对接方案编码(订阅记录可查),标准轨迹服务为 Tracking_JD
|
||||
'lop_dn' => $envJd('LOP_DN', 'Tracking_JD'),
|
||||
// 业务体字段:单号字段名 / 单号类型(20000=运单号) / 商家编码(可选)
|
||||
'reference_field' => $envJd('REFERENCE_FIELD', 'referenceNumber'),
|
||||
'reference_type' => $envJd('REFERENCE_TYPE', '20000'),
|
||||
'customer_code' => $envJd('CUSTOMER_CODE', ''),
|
||||
'api_version' => $envJd('API_VERSION', '2.0'),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user