> runtime/log/gancao_sync.log 2>&1 * * 跳过履约状态为已完成(3)、已签收(6) 的业务订单,不再拉取甘草路由。 * 已对「业务订单创建人非二中心」(发货/签收自动释放规则见 ExpressTrackingService)的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at, * 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。 */ class GancaoSyncLogisticsRoute extends Command { protected function configure() { $this->setName('gancao:sync-logistics') ->setDescription('同步甘草订单的物流路由(GET_TASK_ROUTE_LIST)到本地物流追踪表') ->addOption('limit', 'l', Option::VALUE_OPTIONAL, '本次最多处理多少条订单(跳过已完成/已签收)', 2000) ->addOption('order-id', null, Option::VALUE_OPTIONAL, '只同步指定订单:prescription_order.id 或 PO 业务单号 order_no', null) ->addOption('tracking-number', 't', Option::VALUE_REQUIRED, '按快递单号走快递100 查询并落库') ->addOption('detail', 'd', Option::VALUE_NONE, '打印每单详细结果'); } protected function execute(Input $input, Output $output) { $limit = max(1, (int) $input->getOption('limit')); $onlyOrderId = null; $onlyOrderNo = ''; $orderIdArg = $input->getOption('order-id'); if ($orderIdArg !== null && trim((string) $orderIdArg) !== '') { $resolved = self::resolvePrescriptionOrderId((string) $orderIdArg); if ($resolved === null) { $output->error('未找到订单:' . trim((string) $orderIdArg) . '(--order-id 支持数字 id 或 PO 业务单号)'); return 1; } $onlyOrderId = $resolved['id']; $onlyOrderNo = $resolved['order_no']; } $trackingNumber = trim((string) $input->getOption('tracking-number')); $verbose = (bool) $input->getOption('detail'); if ($trackingNumber !== '' && $onlyOrderId !== null) { $output->error('请勿同时使用 --tracking-number 与 --order-id'); return 1; } $output->writeln('========================================'); $output->writeln($trackingNumber !== '' ? '快递100 物流查询' : '甘草物流路由同步'); $output->writeln('========================================'); if ($trackingNumber === '' && !GancaoScmRecipelService::isConfigured()) { $output->error('甘草 SCM 未配置:' . GancaoScmRecipelService::whyNotConfigured()); return 1; } $start = microtime(true); if ($trackingNumber !== '') { $output->writeln('开始查询...(快递100,tracking_number=' . $trackingNumber . ')'); } else { if ($onlyOrderId !== null) { $output->writeln('开始同步...(指定单 order_id=' . $onlyOrderId . ($onlyOrderNo !== '' ? ', order_no=' . $onlyOrderNo : '') . ')'); } else { $output->writeln('开始同步...(limit=' . $limit . ')'); } } try { if ($trackingNumber !== '') { $stats = ExpressTrackingService::queryKuaidiByTrackingNumber($trackingNumber); $stats['reconcile_cleared'] = 0; $stats['reconcile_scanned'] = 0; $stats['reconcile_lines'] = []; } else { $stats = GancaoLogisticsRouteService::syncBatch($limit, $onlyOrderId); if ($onlyOrderId !== null) { $stats['reconcile_cleared'] = 0; $stats['reconcile_scanned'] = 0; $stats['reconcile_lines'] = []; } else { $recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(2000); $stats['reconcile_cleared'] = (int) ($recon['cleared'] ?? 0); $stats['reconcile_scanned'] = (int) ($recon['scanned'] ?? 0); $stats['reconcile_lines'] = $recon['lines'] ?? []; } } } catch (\Throwable $e) { $output->error('同步异常:' . $e->getMessage()); return 1; } $duration = round(microtime(true) - $start, 2); if ($verbose && !empty($stats['details'])) { $output->writeln(''); $output->writeln('--- 详细 ---'); foreach ($stats['details'] as $row) { $tag = !empty($row['success']) ? '[OK]' : '[FAIL]'; $line = sprintf( '%s order_id=%s order_no=%s app_order_no=%s tn=%s state=%s traces=+%s source=%s msg=%s', $tag, $row['order_id'] ?? '', $row['order_no'] ?? '', $row['app_order_no'] ?? '', $row['tracking_number'] ?? '', $row['state'] ?? '', $row['traces'] ?? 0, $row['source'] ?? '', $row['message'] ?? '' ); $output->writeln($line); } } $output->writeln(''); $output->writeln('========================================'); $output->writeln('同步完成'); $output->writeln('========================================'); $output->writeln('总数:' . $stats['total']); if (isset($stats['skipped'])) { $output->writeln('跳过:' . (int) $stats['skipped'] . '(已完成/已签收)'); } $output->writeln('成功:' . $stats['success']); $output->writeln('失败:' . $stats['failed']); $output->writeln('医助已移除(甘草同步+指派日志):' . (int) ($stats['assistant_cleared'] ?? 0)); $output->writeln('医助已移除(履约已发货核对):' . (int) ($stats['reconcile_cleared'] ?? 0) . '(扫描 ' . (int) ($stats['reconcile_scanned'] ?? 0) . ' 单)'); $lines = array_merge($stats['assistant_lines'] ?? [], $stats['reconcile_lines'] ?? []); if ($lines === []) { $output->writeln('医助明细:(无)'); } else { $output->writeln('医助明细:'); foreach ($lines as $line) { $output->writeln(' ' . $line); } } $output->writeln('耗时:' . $duration . 's'); return 0; } /** * @return array{id:int, order_no:string}|null */ private static function resolvePrescriptionOrderId(string $raw): ?array { $raw = trim($raw); if ($raw === '') { return null; } $q = PrescriptionOrder::whereNull('delete_time'); if (preg_match('/^\d+$/', $raw) === 1) { $id = (int) $raw; if ($id <= 0) { return null; } $row = (clone $q)->where('id', $id)->field('id,order_no')->find(); } else { $row = (clone $q)->where('order_no', $raw)->field('id,order_no')->find(); } if (!$row) { return null; } return [ 'id' => (int) $row->id, 'order_no' => (string) $row->order_no, ]; } }