setName('shopro:auto-confirm-overdue') ->addOption('dry-run', null, Option::VALUE_NONE, '只列出将入队的订单,不推送队列') ->addOption('limit', null, Option::VALUE_REQUIRED, '最多处理条数,默认 100', 100) ->addOption('days', null, Option::VALUE_REQUIRED, '覆盖配置的自动收货天数(不传则读 order_auto_confirm)') ->addOption('force', null, Option::VALUE_NONE, '忽略短时去重缓存,强制重新入队') ->setDescription('Re-queue overdue shipped order items into shopro autoConfirm'); } protected function execute(Input $input, Output $output) { $dryRun = (bool) $input->getOption('dry-run'); $force = (bool) $input->getOption('force'); $limit = max(1, (int) $input->getOption('limit')); $days = $this->resolveDays($input); $deadline = time() - ($days * 86400); $output->writeln(sprintf( '[info] order_auto_confirm=%d day(s), deadline<=%s, limit=%d%s', $days, date('Y-m-d H:i:s', $deadline), $limit, $dryRun ? ', dry-run' : '' )); $items = OrderItem::where('dispatch_status', OrderItem::DISPATCH_STATUS_SENDED) ->where('dispatch_type', '<>', 'selfetch') ->where('refund_status', 'not in', [ OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH, ]) ->order('id', 'asc') ->select(); $queued = 0; $skipped = 0; $matched = 0; foreach ($items as $item) { $sendTime = $this->extractSendTime($item); if ($sendTime <= 0) { $skipped++; continue; } if ($sendTime > $deadline) { continue; } $order = Order::get($item['order_id']); if (!$order) { $output->writeln(sprintf( '[SKIP] item#%d order#%d 订单不存在或已删除', $item['id'], $item['order_id'] )); $skipped++; continue; } $matched++; $line = sprintf( 'order#%d %s item#%d send_time=%s overdue_days=%d', $order['id'], $order['order_sn'] ?? '', $item['id'], date('Y-m-d H:i:s', $sendTime), (int) floor((time() - $sendTime) / 86400) ); if ($dryRun) { $output->writeln('[dry-run] ' . $line); if ($matched >= $limit) { break; } continue; } $cacheKey = 'shopro:auto_confirm_requeue:' . $item['id']; if (!$force && Cache::get($cacheKey)) { $output->writeln('[SKIP-DEDUP] ' . $line); $skipped++; continue; } try { // 仅传 id,与 Job 内重新查询一致,避免大对象序列化 Queue::push( '\addons\shopro\job\OrderAutoOper@autoConfirm', [ 'order' => ['id' => (int) $order['id']], 'item' => ['id' => (int) $item['id']], ], 'shopro' ); Cache::set($cacheKey, 1, 3600); $queued++; $output->writeln('[QUEUED] ' . $line); } catch (\Exception $e) { $output->writeln('[FAIL] ' . $line . ' err=' . $e->getMessage()); \think\Log::write( 'shopro:auto-confirm-overdue push fail item=' . $item['id'] . ' ' . $e->getMessage() ); } if ($queued >= $limit) { break; } } $output->writeln(sprintf( '[done] matched=%d queued=%d skipped=%d%s', $matched, $queued, $skipped, $dryRun ? ' (dry-run, no push)' : '' )); return 0; } protected function resolveDays(Input $input) { $override = $input->getOption('days'); if ($override !== null && $override !== false && $override !== '') { $days = (int) $override; return $days > 0 ? $days : 10; } $config = Config::where('name', 'order')->find(); $cfg = $config ? json_decode($config['value'], true) : []; if (!is_array($cfg)) { $cfg = []; } $days = (isset($cfg['order_auto_confirm']) && $cfg['order_auto_confirm'] > 0) ? (int) $cfg['order_auto_confirm'] : 10; return $days; } protected function extractSendTime($item) { $ext = []; if (!empty($item['ext'])) { $decoded = json_decode($item['ext'], true); if (is_array($decoded)) { $ext = $decoded; } } if (!empty($ext['send_time'])) { return (int) $ext['send_time']; } return 0; } }