订单ID, * 'order_type' => 订单类型, * 'tracking_number' => 快递单号, * 'express_company' => 快递公司, * 'recipient_phone' => 收件人手机, * 'recipient_name' => 收件人姓名, * 'recipient_address' => 收件地址, * ] * @return ExpressTracking|null */ public static function createOrUpdate(array $params): ?ExpressTracking { $trackingNumber = trim($params['tracking_number'] ?? ''); if ($trackingNumber === '') { return null; } $tracking = ExpressTracking::where('tracking_number', $trackingNumber) ->whereNull('delete_time') ->find(); $now = time(); if (!$tracking) { $tracking = new ExpressTracking(); $tracking->tracking_number = $trackingNumber; $tracking->create_time = $now; } $tracking->order_id = (int) ($params['order_id'] ?? 0); $tracking->order_type = (string) ($params['order_type'] ?? 'prescription'); $tracking->express_company = (string) ($params['express_company'] ?? 'auto'); $tracking->recipient_phone = (string) ($params['recipient_phone'] ?? ''); $tracking->recipient_name = (string) ($params['recipient_name'] ?? ''); $tracking->recipient_address = (string) ($params['recipient_address'] ?? ''); $tracking->update_time = $now; // 设置下次更新时间(立即查询) if ($tracking->next_update_time === 0) { $tracking->next_update_time = $now; } $tracking->save(); // 立即查询一次物流信息 self::queryAndUpdate($tracking->id); return $tracking; } /** * 查询并更新物流信息 * * @param int $trackingId * @param bool $isAuto 是否自动查询 * @return array|null */ public static function queryAndUpdate(int $trackingId, bool $isAuto = false): ?array { $tracking = ExpressTracking::where('id', $trackingId) ->whereNull('delete_time') ->find(); if (!$tracking) { return null; } self::backfillRecipientPhoneFromPrescriptionOrder($tracking); $startTime = microtime(true); // 调用快递100查询(顺丰等单号需带收件人手机号后四位,见 ExpressTrackService) $result = ExpressTrackService::query( $tracking->express_company, $tracking->tracking_number, (string) $tracking->recipient_phone ); $responseTime = (int) ((microtime(true) - $startTime) * 1000); // 记录查询日志 self::logQuery( $tracking->tracking_number, $tracking->express_company, $isAuto ? 'auto' : 'manual', $result, $responseTime ); // 更新追踪记录 $now = time(); $oldState = $tracking->current_state; $tracking->express_company_name = $result['carrier_label'] ?? ''; $tracking->current_state = $result['state'] ?? '0'; $tracking->current_state_text = $result['state_text'] ?? ''; $tracking->data_source = $result['source'] ?? ''; $tracking->last_query_time = $now; $tracking->query_count = $tracking->query_count + 1; // 更新预计信息 if (!empty($result['arrivalTime'])) { $tracking->estimated_arrival_time = $result['arrivalTime']; } if (!empty($result['totalTime'])) { $tracking->total_time = $result['totalTime']; } if (!empty($result['remainTime'])) { $tracking->remain_time = $result['remainTime']; } // 更新路由信息 if (!empty($result['routeInfo'])) { $tracking->route_info = json_encode($result['routeInfo'], JSON_UNESCAPED_UNICODE); } // 更新最新轨迹 if (!empty($result['traces']) && is_array($result['traces'])) { $latestTrace = $result['traces'][0]; $tracking->latest_trace_time = $latestTrace['time'] ?? ''; $tracking->latest_trace_context = $latestTrace['context'] ?? ''; $tracking->latest_location = $latestTrace['location'] ?? ''; // 保存轨迹明细 self::saveTraces($tracking->id, $tracking->tracking_number, $result['traces']); } // 检查是否签收 if (ExpressTracking::isFinalState($tracking->current_state)) { $tracking->is_signed = 1; $tracking->sign_time = $now; $tracking->auto_update = 0; // 停止自动更新 } // 计算下次更新时间 if ($tracking->auto_update == 1 && !ExpressTracking::isFinalState($tracking->current_state)) { $tracking->next_update_time = $now + $tracking->update_interval; } $tracking->update_time = $now; $tracking->save(); // 记录状态变更 if ($oldState !== $tracking->current_state) { self::logStateChange($tracking, $oldState); } return $result; } /** * 物流表未存收件人手机时,从关联业务订单回填(顺丰/京东等查件必填后四位) */ private static function backfillRecipientPhoneFromPrescriptionOrder(ExpressTracking $tracking): void { $current = trim((string) $tracking->recipient_phone); if ($current !== '') { return; } if ((string) $tracking->order_type !== 'prescription' || (int) $tracking->order_id <= 0) { return; } $po = PrescriptionOrder::where('id', (int) $tracking->order_id) ->whereNull('delete_time') ->find(); if ($po === null) { return; } $p = trim((string) ($po->recipient_phone ?? '')); if ($p !== '') { $tracking->recipient_phone = $p; } } /** * 保存轨迹明细 */ private static function saveTraces(int $trackingId, string $trackingNumber, array $traces): void { foreach ($traces as $trace) { $traceTime = $trace['time'] ?? $trace['ftime'] ?? ''; if ($traceTime === '') { continue; } // 检查是否已存在 $exists = ExpressTrace::where('tracking_id', $trackingId) ->where('trace_time', $traceTime) ->where('trace_context', $trace['context'] ?? '') ->count(); if ($exists > 0) { continue; } $model = new ExpressTrace(); $model->tracking_id = $trackingId; $model->tracking_number = $trackingNumber; $model->trace_time = $traceTime; $model->trace_time_stamp = strtotime($traceTime) ?: time(); $model->trace_context = $trace['context'] ?? ''; $model->status = $trace['status'] ?? ''; $model->status_code = $trace['statusCode'] ?? ''; $model->location = $trace['location'] ?? ''; $model->area_code = $trace['areaCode'] ?? ''; $model->area_name = $trace['areaName'] ?? ''; $model->area_center = $trace['areaCenter'] ?? ''; $model->extra_data = json_encode($trace, JSON_UNESCAPED_UNICODE); $model->create_time = time(); $model->save(); } } /** * 记录状态变更 */ private static function logStateChange(ExpressTracking $tracking, string $oldState): void { $log = new ExpressStateLog(); $log->tracking_id = $tracking->id; $log->tracking_number = $tracking->tracking_number; $log->old_state = $oldState; $log->old_state_text = ExpressTracking::getStateText($oldState); $log->new_state = $tracking->current_state; $log->new_state_text = $tracking->current_state_text; $log->change_time = time(); $log->change_reason = $tracking->latest_trace_context; $log->create_time = time(); $log->save(); // 检查是否需要通知 $shouldNotify = false; // 签收通知 if ($tracking->notify_on_sign == 1 && ExpressTracking::isFinalState($tracking->current_state)) { $shouldNotify = true; } // 异常通知 if ($tracking->notify_on_problem == 1 && ExpressTracking::isProblemState($tracking->current_state)) { $shouldNotify = true; } if ($shouldNotify) { self::sendNotification($tracking, $log); } } /** * 发送通知(可扩展对接企业微信、短信等) */ private static function sendNotification(ExpressTracking $tracking, ExpressStateLog $log): void { // TODO: 对接通知渠道 // 1. 企业微信消息 // 2. 短信通知 // 3. 系统内通知 Log::info('Express state changed notification', [ 'tracking_number' => $tracking->tracking_number, 'old_state' => $log->old_state_text, 'new_state' => $log->new_state_text, 'context' => $log->change_reason, ]); $log->is_notified = 1; $log->notify_time = time(); $log->notify_result = 'logged'; $log->save(); } /** * 记录查询日志 */ private static function logQuery( string $trackingNumber, string $expressCompany, string $queryType, array $result, int $responseTime ): void { $log = new ExpressQueryLog(); $log->tracking_number = $trackingNumber; $log->express_company = $expressCompany; $log->query_type = $queryType; $log->query_source = $result['source'] ?? ''; $log->query_time = time(); $log->is_success = !empty($result['traces']) ? 1 : 0; $log->error_code = $result['hint'] ? '400' : ''; $log->error_message = $result['hint'] ?? ''; $log->response_time = $responseTime; $log->trace_count = count($result['traces'] ?? []); $log->create_time = time(); $log->save(); } /** * 批量自动更新(定时任务调用) * * @param int $limit 每次处理数量 * @return array */ public static function autoUpdateBatch(int $limit = 50): array { $now = time(); $poTable = (new PrescriptionOrder())->getTable(); // 查询需要更新的记录 // 条件: // 1. auto_update = 1(启用自动更新) // 2. next_update_time <= now(到达更新时间) // 3. is_signed = 0(未签收) // 4. current_state 不是终态(排除已签收、退签、拒签) // 5. 关联业务订单 tcm_prescription_order:已存在甘草单号 gancao_reciperl_order_no 的不再走本任务(由甘草侧物流拉取) $list = ExpressTracking::alias('et') ->leftJoin("{$poTable} po", "et.order_id = po.id AND et.order_type = 'prescription'") ->where('et.auto_update', 1) ->where('et.next_update_time', '<=', $now) ->where('et.is_signed', 0) ->whereNotIn('et.current_state', [ ExpressTracking::STATE_SIGNED, ExpressTracking::STATE_RETURN_SIGNED, ExpressTracking::STATE_REJECTED, ]) ->whereNull('et.delete_time') ->whereRaw( "(et.order_type <> ? OR TRIM(COALESCE(po.gancao_reciperl_order_no, '')) = '')", ['prescription'] ) ->field('et.*') ->limit($limit) ->select(); $success = 0; $failed = 0; foreach ($list as $tracking) { try { self::queryAndUpdate($tracking->id, true); $success++; } catch (\Throwable $e) { $failed++; Log::error('Auto update express tracking failed', [ 'tracking_id' => $tracking->id, 'tracking_number' => $tracking->tracking_number, 'error' => $e->getMessage(), ]); } } return [ 'total' => count($list), 'success' => $success, 'failed' => $failed, ]; } /** * 获取追踪详情(包含轨迹) */ public static function getDetail(int $trackingId): ?array { $tracking = ExpressTracking::with(['traces', 'stateLogs']) ->where('id', $trackingId) ->whereNull('delete_time') ->find(); if (!$tracking) { return null; } $data = $tracking->toArray(); $data['traces'] = $tracking->traces ? $tracking->traces->toArray() : []; $data['state_logs'] = $tracking->stateLogs ? $tracking->stateLogs->toArray() : []; return $data; } /** * 根据快递单号获取追踪详情(包含轨迹) */ public static function getDetailByTrackingNumber(string $trackingNumber): ?array { $tracking = ExpressTracking::with(['traces' => function($query) { // 按时间倒序排列(最新的在前) $query->order('trace_time_stamp', 'desc'); }]) ->where('tracking_number', $trackingNumber) ->whereNull('delete_time') ->find(); if (!$tracking) { return null; } $data = $tracking->toArray(); $data['traces'] = $tracking->traces ? $tracking->traces->toArray() : []; return $data; } }