feat(order): 新增自动确认过期命令并优化确认收货队列逻辑
- 注册 AutoConfirmOverdue 命令行任务以处理过期订单。 - 优化 autoConfirm 方法,增强参数提取与数据查询的健壮性。 - 增加确认收货成功与跳过的详细日志,便于后续问题排查。 - 新增 autoConfirmSkipReason 方法,精准区分任务跳过原因。 - 完善异常捕获注释,明确失败时不删除任务以便回流重试。
这个提交包含在:
@@ -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'];
|
||||
|
||||
在新工单中引用
屏蔽一个用户