diff --git a/addons/shopro/Shopro.php b/addons/shopro/Shopro.php index 53015c2..58a5158 100644 --- a/addons/shopro/Shopro.php +++ b/addons/shopro/Shopro.php @@ -84,7 +84,8 @@ class Shopro extends Addons if (request()->isCli()) { \think\Console::addDefaultCommands([ - 'addons\shopro\command\Chat' + 'addons\shopro\command\Chat', + 'addons\shopro\command\AutoConfirmOverdue', ]); } } diff --git a/addons/shopro/command/AutoConfirmOverdue.php b/addons/shopro/command/AutoConfirmOverdue.php new file mode 100644 index 0000000..261e70b --- /dev/null +++ b/addons/shopro/command/AutoConfirmOverdue.php @@ -0,0 +1,176 @@ +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; + } +} diff --git a/addons/shopro/job/OrderAutoOper.php b/addons/shopro/job/OrderAutoOper.php index c2c07e3..f5cdbe6 100644 --- a/addons/shopro/job/OrderAutoOper.php +++ b/addons/shopro/job/OrderAutoOper.php @@ -56,32 +56,64 @@ class OrderAutoOper extends BaseJob */ public function autoConfirm(Job $job, $data) { try { - $order = $data['order']; - $item = $data['item']; + $orderData = $data['order'] ?? null; + $itemData = $data['item'] ?? null; + $orderId = (is_array($orderData) || is_object($orderData)) ? (int) ($orderData['id'] ?? 0) : 0; + $itemId = (is_array($itemData) || is_object($itemData)) ? (int) ($itemData['id'] ?? 0) : 0; // 重新查询订单 - $order = Order::get($order['id']); - + $order = $orderId ? Order::get($orderId) : null; + // 只要没有退款成功,所有队列正常执行 - $item = OrderItem::where('id', $item['id']) + $item = $itemId ? OrderItem::where('id', $itemId) ->where('dispatch_status', OrderItem::DISPATCH_STATUS_SENDED) ->where('refund_status', 'not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]) - ->find(); + ->find() : null; if ($order && $item) { - \think\Db::transaction(function () use ($order, $item, $data) { + \think\Db::transaction(function () use ($order, $item) { (new Order())->getedItem($order, $item, ['oper_type' => 'system']); }); + \think\Log::write('queue-' . get_class() . '-autoConfirm:确认收货成功 order_id=' . $orderId . ' item_id=' . $itemId); + } else { + $reason = $this->autoConfirmSkipReason($orderId, $itemId, $order); + \think\Log::write('queue-' . get_class() . '-autoConfirm:跳过 ' . $reason); } // 删除 job $job->delete(); } catch (\Exception $e) { - // 队列执行失败 + // 队列执行失败(不 delete,便于 reserved 回流重试) \think\Log::write('queue-' . get_class() . '-autoConfirm' . ':执行失败,错误信息:' . $e->getMessage()); } } + /** + * 自动确认跳过原因(便于区分丢任务 vs 空跑) + */ + protected function autoConfirmSkipReason($orderId, $itemId, $order) + { + if (!$orderId || !$itemId) { + return 'payload缺少order/item id'; + } + if (!$order) { + return 'order_id=' . $orderId . ' 订单不存在'; + } + $raw = OrderItem::where('id', $itemId)->find(); + if (!$raw) { + return 'order_id=' . $orderId . ' item_id=' . $itemId . ' 明细不存在'; + } + if ((int) $raw['dispatch_status'] !== OrderItem::DISPATCH_STATUS_SENDED) { + return 'order_id=' . $orderId . ' item_id=' . $itemId + . ' dispatch_status=' . $raw['dispatch_status'] . '(非已发货)'; + } + if (in_array((int) $raw['refund_status'], [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH], true)) { + return 'order_id=' . $orderId . ' item_id=' . $itemId + . ' refund_status=' . $raw['refund_status'] . '(已退款)'; + } + return 'order_id=' . $orderId . ' item_id=' . $itemId . ' 未知原因'; + } + public function autoComment(Job $job, $data) { try { $order = $data['order'];