订单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 快递100 查询结果;自动任务时可能含 _assistant_sync(医助移除/跳过) */ 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; // 停止自动更新 } // 物流为「签收」(STATE_SIGNED) → 自动推进关联业务订单到「已签收」(fulfillment_status=6) // 排除「退签/拒签」等其它终态,避免误把退/拒签的订单标成已签收 if ($tracking->current_state === ExpressTracking::STATE_SIGNED) { self::autoMarkOrderAsSigned($tracking); } // 计算下次更新时间 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); } // 定时拉轨迹:履约已发货/已签收时与甘草侧共用 applyAssistantReleaseForShippedPrescriptionOrder,释放诊单医助并写指派日志 $assistantSync = null; if ($isAuto) { $assistantSync = self::maybeClearDiagnosisAssistantWhenShippedPrescription($tracking); } return $assistantSync !== null ? array_merge($result, ['_assistant_sync' => $assistantSync]) : $result; } /** * 物流签收时,自动将关联处方业务订单的履约状态推进到「已签收」(6) * * 仅在当前 fulfillment_status 属于「待双审通过(1)/待发货(2)/已发货(5)」时升级, * 避免覆盖「已完成(3)/已取消(4)/已签收(6)/拒收(9)/退款(10)」等业务终态。 */ private static function autoMarkOrderAsSigned(ExpressTracking $tracking): void { if ((string) $tracking->order_type !== 'prescription') { return; } $orderId = (int) $tracking->order_id; if ($orderId <= 0) { return; } try { $order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find(); if (!$order) { return; } $currentFs = (int) $order->fulfillment_status; if (!in_array($currentFs, [1, 2, 5], true)) { return; } $order->fulfillment_status = 6; $order->save(); try { $log = new PrescriptionOrderLog(); $log->prescription_order_id = $orderId; $log->admin_id = 0; $log->admin_name = '系统'; $log->action = 'auto_sign'; $log->summary = mb_substr( '物流签收(运单:' . (string) $tracking->tracking_number . '),订单状态自动更新为「已签收」', 0, 500 ); $log->create_time = time(); $log->save(); } catch (\Throwable $e) { Log::warning('Auto sign order log save failed: ' . $e->getMessage(), [ 'order_id' => $orderId, ]); } } catch (\Throwable $e) { Log::warning('Auto sign order failed: ' . $e->getMessage(), [ 'tracking_id' => (int) $tracking->id, 'order_id' => $orderId, ]); } } /** * 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。 * 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。 * 不自动清空的情形:(1)按业务订单 creator_id(代建场景)或回退身份判断是否在「二中心」部门子树;(2)诊单已有 shipped_non_er_assistant_cleared_at 且 assistant_id 已为 0(视为已处理);(3)指派日志存在后台人工指派:`operator_admin_id>0` 且 `to_assistant_id>0`(系统任务为 operator_admin_id=0);或 related_po_creator_id=from_assistant_id 且 to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链。 * * @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta * @return array{ * action: 'cleared', * source?: string, * diagnosis_id: int, * prescription_order_id: int, * tracking_id: int, * tracking_number: string, * former_assistant_id?: string, * fulfillment_status?: int * }|null */ public static function applyAssistantReleaseForShippedPrescriptionOrder(PrescriptionOrder $order, array $meta = []): ?array { $orderId = (int) $order->id; if ($orderId <= 0) { return null; } $source = (string) ($meta['source'] ?? ''); $trackingId = (int) ($meta['tracking_id'] ?? 0); $tn = (string) ($meta['tracking_number'] ?? $order->tracking_number ?? ''); try { $fs = (int) $order->fulfillment_status; if (!in_array($fs, [5, 6], true)) { return null; } $diagnosisId = (int) ($order->diagnosis_id ?? 0); if ($diagnosisId <= 0) { return null; } $diag = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->find(); if (!$diag) { return null; } // 后台曾人工指派到新医助(operator_admin_id>0 且 to>0,与系统写入的 operator=0 区分)时不再自动清空;另保留快照 related_po=from 的旧数据兼容 if (self::diagnosisShouldSkipAutoAssistantReleaseDueToManualAssignLog($diagnosisId)) { Log::info('Skipped auto assistant release (shipped/signed): manual assign history exists (human operator + to_assistant_id>0, or snapshot-tied pattern)', [ 'diagnosis_id' => $diagnosisId, 'prescription_order_id' => $orderId, 'source' => $source, ]); return null; } $shippedClearedAt = (int) ($diag->getAttr('shipped_non_er_assistant_cleared_at') ?? 0); $diagAssistantIdNow = (int) ($diag->getAttr('assistant_id') ?? 0); if ($shippedClearedAt > 0 && $diagAssistantIdNow === 0) { return null; } $poCreatorId = (int) ($order->getAttr('creator_id') ?? 0); $rxId = (int) ($order->getAttr('prescription_id') ?? 0); $rxCreator = 0; if ($rxId > 0) { $rxCreator = (int) Prescription::where('id', $rxId)->whereNull('delete_time')->value('creator_id'); } // 诊单 assistant_id 与列表「医助」筛选一致:医助常代建单但诊单未写 assistant_id,此时用创建人作为待释放身份(创建人即开方医生时不采用,避免误记指派日志) $fromAssistantId = (int) ($diag->getAttr('assistant_id') ?? 0); if ($fromAssistantId <= 0) { if ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator)) { $fromAssistantId = $poCreatorId; } } if ($fromAssistantId <= 0) { return null; } /** 是否因「二中心」跳过释放:优先看业务订单创建人 creator_id(代建场景);创建人即开方医生时用 fromAssistantId */ $erZhongxinCheckAdminId = ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator)) ? $poCreatorId : $fromAssistantId; if ($erZhongxinCheckAdminId > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($erZhongxinCheckAdminId)) { Log::info('Skipped auto assistant release (shipped/signed): order creator (or fallback from-id) under 二中心 dept tree', [ 'diagnosis_id' => $diagnosisId, 'prescription_order_id' => $orderId, 'source' => $source, 'er_zhongxin_check_admin_id' => $erZhongxinCheckAdminId, 'prescription_order_creator_id' => $poCreatorId, 'from_assistant_id_for_log' => $fromAssistantId, ]); return null; } $operatorName = match ($source) { 'express_auto_update' => '系统·物流自动同步', 'gancao_route', 'gancao_route_sync' => '系统·甘草路由同步', 'gancao_callback' => '系统·甘草回调', 'shipped_fulfillment_reconcile' => '系统·已发货履约核对', default => '系统·订单已发货/签收', }; $relatedPoCreatorId = (int) ($order->getAttr('creator_id') ?? 0); $relatedPoCreateTime = (int) ($order->getAttr('create_time') ?? 0); Db::startTrans(); try { Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->update([ 'assistant_id' => 0, 'shipped_non_er_assistant_cleared_at' => time(), ]); DiagnosisAssignLog::create([ 'diagnosis_id' => $diagnosisId, 'from_assistant_id' => $fromAssistantId, 'to_assistant_id' => 0, 'operator_admin_id' => 0, 'operator_name' => $operatorName, 'operator_account' => '', 'ip' => '', 'related_po_creator_id' => $relatedPoCreatorId, 'related_po_create_time' => $relatedPoCreateTime, 'create_time' => time(), ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } Log::info('Cleared diagnosis assistant (prescription shipped/signed), assign log written', [ 'source' => $source, 'diagnosis_id' => $diagnosisId, 'prescription_order_id' => $orderId, 'tracking_id' => $trackingId, 'fulfillment_status' => $fs, 'former_assistant_id' => $fromAssistantId, 'related_po_creator_id' => $relatedPoCreatorId, 'related_po_create_time' => $relatedPoCreateTime, 'assistant_created_order' => ($relatedPoCreatorId === $fromAssistantId && $relatedPoCreateTime > 0) ? 1 : 0, ]); return [ 'action' => 'cleared', 'source' => $source, 'diagnosis_id' => $diagnosisId, 'prescription_order_id' => $orderId, 'tracking_id' => $trackingId, 'tracking_number' => $tn, 'former_assistant_id' => (string) $fromAssistantId, 'fulfillment_status' => $fs, ]; } catch (\Throwable $e) { Log::warning('applyAssistantReleaseForShippedPrescriptionOrder failed: ' . $e->getMessage(), [ 'source' => $source, 'order_id' => $orderId, ]); return null; } } /** * 部门名称为「二中心」的根节点 id(启用、未删除的首条),无则 0。 * 与后台 adminapi 部门数据源一致(表 zyt_dept,树由 pid 构成)。 */ private static function getErzhongxinRootDeptId(): int { static $cached = null; if ($cached !== null) { return $cached; } $id = (int) Dept::where('name', '二中心') ->where('status', 1) ->whereNull('delete_time') ->order('id', 'asc') ->value('id'); $cached = $id > 0 ? $id : 0; return $cached; } /** * 诊单当前医助(管理员)是否归属「二中心」或其任意下级部门(admin_dept.dept_id 落在该子树内)。 * 与 DeptLogic::getSelfAndDescendantIds 行为对齐于部门列表接口的树展开。 */ private static function currentAssistantBelongsToErzhongxinDeptTree(int $assistantAdminId): bool { if ($assistantAdminId <= 0) { return false; } $rootId = self::getErzhongxinRootDeptId(); if ($rootId <= 0) { return false; } $treeIds = DeptLogic::getSelfAndDescendantIds($rootId); if ($treeIds === []) { return false; } $allowed = array_flip(array_map('intval', $treeIds)); $deptIds = AdminDept::where('admin_id', $assistantAdminId)->column('dept_id'); foreach ($deptIds as $did) { if (isset($allowed[(int) $did])) { return true; } } return false; } /** * 若诊单曾被后台人工指派到具体医助,则不再执行发运/签收自动清空(避免「系统·已发货履约核对」覆盖人工链)。 * 判定:operator_admin_id>0 且 to_assistant_id>0(人工);系统写入的释放/同步为 operator_admin_id=0。 * 另保留 related_po_creator_id=from_assistant_id 且 to>0 的窄条件,兼容早期数据。 */ private static function diagnosisShouldSkipAutoAssistantReleaseDueToManualAssignLog(int $diagnosisId): bool { if ($diagnosisId <= 0) { return false; } $tbl = 'tcm_diagnosis_assign_log'; $humanReassign = (int) Db::name($tbl) ->where('diagnosis_id', $diagnosisId) ->where('to_assistant_id', '>', 0) ->where('operator_admin_id', '>', 0) ->count(); if ($humanReassign > 0) { return true; } $snapshotTied = (int) Db::name($tbl) ->where('diagnosis_id', $diagnosisId) ->where('to_assistant_id', '>', 0) ->where('from_assistant_id', '>', 0) ->where('related_po_creator_id', '>', 0) ->whereRaw('`related_po_creator_id` = `from_assistant_id`') ->count(); return $snapshotTied > 0; } private static function maybeClearDiagnosisAssistantWhenShippedPrescription(ExpressTracking $tracking): ?array { if ((string) $tracking->order_type !== 'prescription') { return null; } $orderId = (int) $tracking->order_id; if ($orderId <= 0) { return null; } try { $order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find(); if (!$order) { return null; } return self::applyAssistantReleaseForShippedPrescriptionOrder($order, [ 'tracking_id' => (int) $tracking->id, 'tracking_number' => (string) $tracking->tracking_number, 'source' => 'express_auto_update', ]); } catch (\Throwable $e) { Log::warning('maybeClearDiagnosisAssistantWhenShippedPrescription failed: ' . $e->getMessage(), [ 'tracking_id' => (int) $tracking->id, 'order_id' => $orderId, ]); return null; } } /** * 物流表未存收件人手机时,从关联业务订单回填(顺丰/京东等查件必填后四位) */ 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(); } /** * 按处方订单履约核对:已发货(5)/已签收(6) 时符合条件的订单调用 applyAssistantReleaseForShippedPrescriptionOrder。 * 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看业务单创建人**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。 * * @return array{scanned: int, cleared: int, lines: list} */ public static function reconcileAssistantReleaseForShippedPrescriptionOrders(int $limit = 200): array { $dTable = (new Diagnosis())->getTable(); $rxTable = (new Prescription())->getTable(); $orders = PrescriptionOrder::alias('po') ->join($dTable . ' d', 'd.id = po.diagnosis_id') ->leftJoin($rxTable . ' rx', 'rx.id = po.prescription_id AND rx.delete_time IS NULL') ->whereNull('po.delete_time') ->whereNull('d.delete_time') ->whereRaw('(IFNULL(d.shipped_non_er_assistant_cleared_at, 0) = 0 OR d.assistant_id > 0)') ->whereIn('po.fulfillment_status', [5, 6]) ->where('po.diagnosis_id', '>', 0) ->where(function ($w) { $w->where('d.assistant_id', '>', 0) ->whereOr(function ($w2) { $w2->where('po.creator_id', '>', 0) ->whereRaw('(rx.id IS NULL OR po.creator_id <> rx.creator_id)'); }); }) ->order('po.id', 'asc') ->limit($limit) ->field('po.*') ->select(); $cleared = 0; $lines = []; foreach ($orders as $order) { $sync = self::applyAssistantReleaseForShippedPrescriptionOrder($order, [ 'source' => 'shipped_fulfillment_reconcile', 'tracking_number' => (string) ($order->tracking_number ?? ''), ]); if (is_array($sync) && ($sync['action'] ?? '') === 'cleared') { $cleared++; $lines[] = sprintf( '[履约核对·已发货/签收] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d', (int) ($sync['diagnosis_id'] ?? 0), (int) ($sync['prescription_order_id'] ?? 0), (string) ($sync['tracking_number'] ?? ''), (string) ($sync['former_assistant_id'] ?? ''), (int) ($sync['fulfillment_status'] ?? 0) ); } } return [ 'scanned' => count($orders), 'cleared' => $cleared, 'lines' => $lines, ]; } /** * 批量自动更新(定时任务调用) * * @param int $limit 每次处理数量 * @return array{ * total: int, * success: int, * failed: int, * assistant_cleared: int, * assistant_skipped_assign_log: int, * assistant_lines: list * } assistant_skipped_assign_log 保留兼容恒为 0 */ 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 的不再走本任务(由甘草侧物流拉取) // 6. 关联业务订单 fulfillment_status 不在终态白名单:3/4/6/8/9/10/11/12(已完成/已取消/已签收/暂不制药/拒收/退款/保留药方/制药缓发) $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'] ) ->whereRaw( "(et.order_type <> ? OR po.id IS NULL OR po.fulfillment_status NOT IN (3,4,6,8,9,10,11,12))", ['prescription'] ) ->field('et.*') ->limit($limit) ->select(); $success = 0; $failed = 0; $assistantCleared = 0; $assistantLines = []; foreach ($list as $tracking) { try { $ret = self::queryAndUpdate($tracking->id, true); $success++; if (is_array($ret) && isset($ret['_assistant_sync']) && is_array($ret['_assistant_sync'])) { $sync = $ret['_assistant_sync']; $act = (string) ($sync['action'] ?? ''); if ($act === 'cleared') { $assistantCleared++; $assistantLines[] = sprintf( '[移除医助+指派日志] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d', (int) ($sync['diagnosis_id'] ?? 0), (int) ($sync['prescription_order_id'] ?? 0), (string) ($sync['tracking_number'] ?? ''), (string) ($sync['former_assistant_id'] ?? ''), (int) ($sync['fulfillment_status'] ?? 0) ); } } } 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, 'assistant_cleared' => $assistantCleared, 'assistant_skipped_assign_log' => 0, 'assistant_lines' => $assistantLines, ]; } /** * 获取追踪详情(包含轨迹) */ 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; } }