feat(order): 新增自动确认过期命令并优化确认收货队列逻辑

- 注册 AutoConfirmOverdue 命令行任务以处理过期订单。
- 优化 autoConfirm 方法,增强参数提取与数据查询的健壮性。
- 增加确认收货成功与跳过的详细日志,便于后续问题排查。
- 新增 autoConfirmSkipReason 方法,精准区分任务跳过原因。
- 完善异常捕获注释,明确失败时不删除任务以便回流重试。
这个提交包含在:
可乐加冰
2026-11-05 08:50:34 +08:00
父节点 929c97e53a
当前提交 c264a7d30c
共有 3 个文件被更改,包括 218 次插入 和 9 次删除

查看文件

@@ -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',
]);
}
}

查看文件

@@ -0,0 +1,176 @@
<?php
namespace addons\shopro\command;
use addons\shopro\model\Config;
use addons\shopro\model\Order;
use addons\shopro\model\OrderItem;
use think\Cache;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\Queue;
/**
* 扫描超期未收货订单,重新推入 shopro 队列执行 OrderAutoOper@autoConfirm
*/
class AutoConfirmOverdue extends Command
{
protected function configure()
{
$this->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;
}
}

查看文件

@@ -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'];