feat: add kuaidi100 logistics sync
This commit is contained in:
@@ -17,6 +17,54 @@ class ExpressCompaniesController
|
||||
]);
|
||||
}
|
||||
|
||||
public function catalog(Request $request)
|
||||
{
|
||||
$keyword = trim((string)$request->input('keyword', ''));
|
||||
$limit = max(1, min(100, (int)$request->input('limit', 30)));
|
||||
|
||||
return api_success([
|
||||
'list' => $this->service()->catalogList($keyword, $limit),
|
||||
'total' => $this->service()->catalogTotal(),
|
||||
'synced_at' => $this->service()->catalogSyncedAt(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncCatalog(Request $request)
|
||||
{
|
||||
try {
|
||||
$result = $this->service()->syncCatalog();
|
||||
} catch (\RuntimeException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error('快递100公司码表同步失败', 500, [
|
||||
'detail' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return api_success($result, '快递100公司码表已同步');
|
||||
}
|
||||
|
||||
public function recognize(Request $request)
|
||||
{
|
||||
$trackingNo = trim((string)$request->input('tracking_no', ''));
|
||||
$companyName = trim((string)$request->input('company_name', $request->input('company_code', '')));
|
||||
if ($trackingNo === '') {
|
||||
return api_error('运单号不能为空', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->service()->recognizeCompany($companyName, $trackingNo);
|
||||
} catch (\RuntimeException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error('快递公司识别失败', 500, [
|
||||
'detail' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return api_success($result);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$id = (int)$request->input('id', 0);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\support\AppraisalEvidenceService;
|
||||
use app\support\MessageDispatcher;
|
||||
use app\support\EnterpriseWebhookService;
|
||||
use app\support\MessageDispatcher;
|
||||
use app\support\OrderLogisticsSyncService;
|
||||
use app\support\WarehouseService;
|
||||
use support\Request;
|
||||
use support\think\Db;
|
||||
@@ -292,6 +293,17 @@ class OrdersController
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
$syncService = new OrderLogisticsSyncService();
|
||||
$sendSyncStatus = $sendLogistics ? $syncService->formatSyncStatus((int)$sendLogistics['id']) : [
|
||||
'provider_status_text' => '',
|
||||
'sync_status_text' => '未同步',
|
||||
'sync_error' => '',
|
||||
];
|
||||
$returnSyncStatus = $returnLogistics ? $syncService->formatSyncStatus((int)$returnLogistics['id']) : [
|
||||
'provider_status_text' => '',
|
||||
'sync_status_text' => '未同步',
|
||||
'sync_error' => '',
|
||||
];
|
||||
|
||||
return api_success([
|
||||
'order_info' => [
|
||||
@@ -376,23 +388,14 @@ class OrdersController
|
||||
'tracking_no' => $sendLogistics['tracking_no'],
|
||||
'tracking_status' => $sendLogistics['tracking_status'],
|
||||
'tracking_status_text' => $this->trackingStatusText($sendLogistics['tracking_status'], 'send_to_center'),
|
||||
'latest_desc' => $this->formatAdminLogisticsDesc(
|
||||
'send_to_center',
|
||||
$sendLogistics['tracking_status'],
|
||||
$sendLogistics['express_company'],
|
||||
$sendLogistics['tracking_no'],
|
||||
$sendLogistics['latest_desc']
|
||||
),
|
||||
'provider_status_text' => $sendSyncStatus['provider_status_text'],
|
||||
'sync_status_text' => $sendSyncStatus['sync_status_text'],
|
||||
'sync_error' => $sendSyncStatus['sync_error'],
|
||||
'latest_desc' => (string)($sendLogistics['latest_desc'] ?? ''),
|
||||
'latest_time' => $sendLogistics['latest_time'],
|
||||
'nodes' => array_map(fn (array $item) => [
|
||||
'node_time' => $item['node_time'],
|
||||
'node_desc' => $this->formatAdminLogisticsDesc(
|
||||
'send_to_center',
|
||||
$sendLogistics['tracking_status'],
|
||||
$sendLogistics['express_company'],
|
||||
$sendLogistics['tracking_no'],
|
||||
$item['node_desc']
|
||||
),
|
||||
'node_desc' => $item['node_desc'],
|
||||
'node_location' => $item['node_location'],
|
||||
], $logisticsNodes),
|
||||
] : null,
|
||||
@@ -402,23 +405,14 @@ class OrdersController
|
||||
'tracking_no' => $returnLogistics['tracking_no'],
|
||||
'tracking_status' => $returnLogistics['tracking_status'],
|
||||
'tracking_status_text' => $this->trackingStatusText($returnLogistics['tracking_status'], 'return_to_user'),
|
||||
'latest_desc' => $this->formatAdminLogisticsDesc(
|
||||
'return_to_user',
|
||||
$returnLogistics['tracking_status'],
|
||||
$returnLogistics['express_company'],
|
||||
$returnLogistics['tracking_no'],
|
||||
$returnLogistics['latest_desc']
|
||||
),
|
||||
'provider_status_text' => $returnSyncStatus['provider_status_text'],
|
||||
'sync_status_text' => $returnSyncStatus['sync_status_text'],
|
||||
'sync_error' => $returnSyncStatus['sync_error'],
|
||||
'latest_desc' => (string)($returnLogistics['latest_desc'] ?? ''),
|
||||
'latest_time' => $returnLogistics['latest_time'],
|
||||
'nodes' => array_map(fn (array $item) => [
|
||||
'node_time' => $item['node_time'],
|
||||
'node_desc' => $this->formatAdminLogisticsDesc(
|
||||
'return_to_user',
|
||||
$returnLogistics['tracking_status'],
|
||||
$returnLogistics['express_company'],
|
||||
$returnLogistics['tracking_no'],
|
||||
$item['node_desc']
|
||||
),
|
||||
'node_desc' => $item['node_desc'],
|
||||
'node_location' => $item['node_location'],
|
||||
], $returnLogisticsNodes),
|
||||
] : null,
|
||||
@@ -833,6 +827,7 @@ class OrdersController
|
||||
'tracking_no' => $trackingNo,
|
||||
'shipped_at' => $now,
|
||||
]);
|
||||
(new OrderLogisticsSyncService())->subscribeAsync($logisticsId);
|
||||
|
||||
return api_success([
|
||||
'id' => $id,
|
||||
|
||||
@@ -448,6 +448,30 @@ class SystemConfigsController
|
||||
['config_key' => 'endpoint', 'title' => '短信 Endpoint', 'field_type' => 'text', 'placeholder' => '默认可留空', 'remark' => '如不填写则按 SDK 默认规则解析', 'is_secret' => false],
|
||||
],
|
||||
],
|
||||
'kuaidi100' => [
|
||||
'group_name' => '快递100',
|
||||
'group_desc' => '配置快递100实时查询与物流订阅推送,用于订单寄送和回寄物流轨迹同步。',
|
||||
'items' => [
|
||||
[
|
||||
'config_key' => 'enabled',
|
||||
'title' => '同步开关',
|
||||
'field_type' => 'select',
|
||||
'placeholder' => '请选择是否启用',
|
||||
'remark' => '启用后,新提交的运单会尝试订阅快递100推送,后台进程会定时补查轨迹。',
|
||||
'is_secret' => false,
|
||||
'default_value' => 'disabled',
|
||||
'options' => [
|
||||
['label' => '停用', 'value' => 'disabled'],
|
||||
['label' => '启用', 'value' => 'enabled'],
|
||||
],
|
||||
],
|
||||
['config_key' => 'customer', 'title' => 'Customer', 'field_type' => 'text', 'placeholder' => '请输入快递100 Customer', 'remark' => '实时查询接口签名使用的 Customer。', 'is_secret' => false, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']],
|
||||
['config_key' => 'key', 'title' => 'Key', 'field_type' => 'password', 'placeholder' => '请输入快递100 Key', 'remark' => '用于实时查询签名和订阅推送。请妥善保管。', 'is_secret' => true, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']],
|
||||
['config_key' => 'callback_url', 'title' => '推送回调地址', 'field_type' => 'text', 'placeholder' => '例如 https://api.example.com/api/open/kuaidi100/callback', 'remark' => '需公网可访问;生产建议填本系统 /api/open/kuaidi100/callback 的完整地址。', 'is_secret' => false, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']],
|
||||
['config_key' => 'callback_salt', 'title' => '回调 Salt', 'field_type' => 'password', 'placeholder' => '可选,需与快递100订阅参数保持一致', 'remark' => '用于快递100推送签名增强;如账号未配置可留空。', 'is_secret' => true, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']],
|
||||
['config_key' => 'query_min_interval_minutes', 'title' => '最小查询间隔(分钟)', 'field_type' => 'text', 'placeholder' => '默认 30', 'remark' => '定时补查同一运单的最小间隔,允许 5-1440。', 'is_secret' => false, 'default_value' => '30', 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -467,6 +491,7 @@ class SystemConfigsController
|
||||
{
|
||||
$driver = (new FileStorageConfigService())->normalizeDriver((string)($configValueMap['file_storage.driver'] ?? 'local'));
|
||||
if ($driver === 'local') {
|
||||
$this->validateKuaidi100Config($configValueMap);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -489,10 +514,12 @@ class SystemConfigsController
|
||||
throw new \RuntimeException('直传文件大小上限需填写 1-2048 之间的整数');
|
||||
}
|
||||
|
||||
$this->validateKuaidi100Config($configValueMap);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($driver !== 'qiniu') {
|
||||
$this->validateKuaidi100Config($configValueMap);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -513,6 +540,35 @@ class SystemConfigsController
|
||||
if ($publicBaseUrl === '' && $bucketDomain === '') {
|
||||
throw new \RuntimeException('当前已切换为七牛云存储,请至少填写公开访问域名或七牛公网访问域名');
|
||||
}
|
||||
|
||||
$this->validateKuaidi100Config($configValueMap);
|
||||
}
|
||||
|
||||
private function validateKuaidi100Config(array $configValueMap): void
|
||||
{
|
||||
$enabled = (string)($configValueMap['kuaidi100.enabled'] ?? 'disabled');
|
||||
if (!in_array($enabled, ['enabled', 'disabled'], true)) {
|
||||
throw new \RuntimeException('快递100同步开关配置无效');
|
||||
}
|
||||
if ($enabled !== 'enabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
$required = [
|
||||
'kuaidi100.customer' => '快递100 Customer',
|
||||
'kuaidi100.key' => '快递100 Key',
|
||||
'kuaidi100.callback_url' => '快递100推送回调地址',
|
||||
];
|
||||
foreach ($required as $key => $label) {
|
||||
if (trim((string)($configValueMap[$key] ?? '')) === '') {
|
||||
throw new \RuntimeException(sprintf('当前已启用快递100,请先填写 %s', $label));
|
||||
}
|
||||
}
|
||||
|
||||
$interval = trim((string)($configValueMap['kuaidi100.query_min_interval_minutes'] ?? '30'));
|
||||
if ($interval !== '' && (!ctype_digit($interval) || (int)$interval < 5 || (int)$interval > 1440)) {
|
||||
throw new \RuntimeException('快递100最小查询间隔需填写 5-1440 之间的整数');
|
||||
}
|
||||
}
|
||||
|
||||
private function applyDerivedConfigValues(array &$configValueMap): void
|
||||
|
||||
Reference in New Issue
Block a user