feat(shopro_wechat_trade): 添加订单确认收货命令

新增 confirm-orders 命令用于商城确认收货并同步微信
notify_confirm_receive,支持批量处理指定订单。

添加了以下功能选项:
- 支持通过 --sn 参数指定订单号进行处理
- 支持通过 --skip-mall 参数跳过商城确认,仅处理微信同步
- 支持 dry-run 模式预览操作
- 在 README.md 中补充了相关使用说明
```
这个提交包含在:
可乐加冰
2026-10-08 14:57:11 +08:00
父节点 48cde5f891
当前提交 a4f307f4ce
共有 3 个文件被更改,包括 197 次插入 和 0 次删除

查看文件

@@ -55,6 +55,11 @@ php think shopro_wechat_trade:verify
php think shopro_wechat_trade:init-path
php think shopro_wechat_trade:backfill
php think shopro_wechat_trade:retry-failed
# 指定订单:商城确认收货 + 微信 confirm(截图里分账 FREEZE 的单可批量处理)
php think shopro_wechat_trade:confirm-orders --sn=202608500438103091004500,202606584688620091002100
# 商城已是「已收货」、只补微信:
php think shopro_wechat_trade:confirm-orders --sn=订单号 --skip-mall
```
## 触发点

查看文件

@@ -0,0 +1,191 @@
<?php
namespace addons\shopro_wechat_trade\command;
use addons\shopro\model\Order;
use addons\shopro\model\OrderItem;
use addons\shopro_wechat_trade\library\Bridge;
use addons\shopro_wechat_trade\library\SyncLogger;
use addons\shopro_wechat_trade\library\WxShippingService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\Db;
/**
* 商城确认收货 + 微信 notify_confirm_receive(按 order_sn / order_id)
*/
class ConfirmOrders extends Command
{
protected function configure()
{
$this->setName('shopro_wechat_trade:confirm-orders')
->addOption('sn', null, Option::VALUE_REQUIRED, '订单号,多个用英文逗号分隔')
->addOption('id', null, Option::VALUE_REQUIRED, '订单 id,多个用英文逗号分隔')
->addOption('skip-mall', null, Option::VALUE_NONE, '跳过商城 getedItem,仅调微信 confirm(商城已全部收货时用)')
->addOption('skip-wechat', null, Option::VALUE_NONE, '仅商城确认收货,不同步微信')
->addOption('dry-run', null, Option::VALUE_NONE, '只打印将执行的操作')
->setDescription('Confirm orders in mall and sync WeChat notify_confirm_receive');
}
protected function execute(Input $input, Output $output)
{
if (!Bridge::isEnabled()) {
$output->writeln('[FAIL] shopro_wechat_trade 未启用或 wechat_trade_enable=0');
return 1;
}
$orderIds = $this->resolveOrderIds($input);
if (!$orderIds) {
$output->writeln('请指定 --sn=订单号 或 --id=订单id');
return 1;
}
$skipMall = (bool) $input->getOption('skip-mall');
$skipWechat = (bool) $input->getOption('skip-wechat');
$dryRun = (bool) $input->getOption('dry-run');
$service = new WxShippingService();
$orderModel = new Order();
$ok = 0;
$fail = 0;
foreach ($orderIds as $orderId) {
$order = Order::with('item')->where('id', $orderId)->find();
if (!$order) {
$output->writeln("[SKIP] 订单不存在 id={$orderId}");
$fail++;
continue;
}
$output->writeln('--- order#' . $order['id'] . ' ' . ($order['order_sn'] ?? '') . ' ---');
if (!Bridge::shouldHandleOrder($order)) {
$output->writeln('[SKIP] 非小程序 wechat/huifu_wechat 或缺少 transaction_id');
$fail++;
continue;
}
if (!$skipMall) {
try {
$this->mallConfirmItems($orderModel, $order, $dryRun, $output);
} catch (\Exception $e) {
$output->writeln(' [FAIL] 商城确认: ' . $e->getMessage());
$fail++;
continue;
}
} else {
$output->writeln(' [info] skip-mall:不修改商城收货状态');
}
if ($skipWechat) {
$output->writeln(' [info] skip-wechat:跳过微信同步');
$ok++;
continue;
}
if (!SyncLogger::hasSuccessfulShipping($orderId)) {
$output->writeln(' [WARN] 无 shipping success,先尝试 upload_shipping_info');
if ($dryRun) {
$output->writeln(' [dry-run] syncOrderShipping');
} else {
try {
$service->syncOrderShipping($orderId);
$output->writeln(' [OK] shipping 同步成功');
} catch (\Exception $e) {
$output->writeln(' [FAIL] shipping: ' . $e->getMessage());
$fail++;
continue;
}
}
}
if (SyncLogger::hasSuccessfulConfirm($orderId)) {
$output->writeln(' [OK] 微信 confirm 已是 success,跳过');
$ok++;
continue;
}
if ($dryRun) {
$output->writeln(' [dry-run] syncOrderConfirm');
$ok++;
continue;
}
try {
$service->syncOrderConfirm($orderId);
$output->writeln(' [OK] 微信 confirm 同步成功');
$ok++;
} catch (\Exception $e) {
$output->writeln(' [FAIL] confirm: ' . $e->getMessage());
$fail++;
}
}
$output->writeln('');
$output->writeln("完成:成功 {$ok},失败 {$fail}");
if (!$skipWechat && !$dryRun && $ok > 0) {
$output->writeln('汇付分账(非 FREEZE 后):php think shopro_huifu:split-retry');
}
return $fail > 0 ? 1 : 0;
}
protected function resolveOrderIds(Input $input)
{
$ids = [];
$snOpt = trim((string) $input->getOption('sn'));
if ($snOpt !== '') {
foreach (preg_split('/[\s,,]+/', $snOpt) as $sn) {
$sn = trim($sn);
if ($sn === '') {
continue;
}
$row = Db::name('shopro_order')->where('order_sn', $sn)->find();
if (!$row && strlen($sn) < 24) {
$row = Db::name('shopro_order')->where('order_sn', 'like', $sn . '%')->order('id', 'desc')->find();
}
if ($row) {
$ids[] = intval($row['id']);
}
}
}
$idOpt = trim((string) $input->getOption('id'));
if ($idOpt !== '') {
foreach (preg_split('/[\s,,]+/', $idOpt) as $id) {
$id = intval($id);
if ($id > 0) {
$ids[] = $id;
}
}
}
return array_values(array_unique($ids));
}
protected function mallConfirmItems(Order $orderModel, $order, $dryRun, Output $output)
{
$items = $order->item ?? [];
foreach ($items as $item) {
$itemArr = is_object($item) && method_exists($item, 'toArray') ? $item->toArray() : (array) $item;
$status = intval($itemArr['dispatch_status'] ?? 0);
if ($status >= OrderItem::DISPATCH_STATUS_GETED) {
continue;
}
if ($status < OrderItem::DISPATCH_STATUS_SENDED) {
$output->writeln(' [FAIL] item#' . ($itemArr['id'] ?? '') . ' 未发货,请先发货再确认');
throw new \RuntimeException('存在未发货商品');
}
if (in_array(intval($itemArr['refund_status'] ?? 0), [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH], true)) {
$output->writeln(' [SKIP] item#' . ($itemArr['id'] ?? '') . ' 已退款');
continue;
}
if ($dryRun) {
$output->writeln(' [dry-run] getedItem item#' . ($itemArr['id'] ?? ''));
continue;
}
Db::transaction(function () use ($orderModel, $order, $item) {
$orderModel->getedItem($order, $item, ['oper_type' => 'admin']);
});
$output->writeln(' [OK] 商城确认 item#' . ($itemArr['id'] ?? ''));
}
}
}

查看文件

@@ -33,6 +33,7 @@ return [
'addons\shopro_wechat_trade\command\ProbeOrderPath',
'addons\shopro_wechat_trade\command\BackfillShipping',
'addons\shopro_wechat_trade\command\RetryFailed',
'addons\shopro_wechat_trade\command\ConfirmOrders',
'addons\shopro_wechat_trade\command\VerifyInstall',
'addons\shopro_wechat_trade\command\InstallFull',
'addons\shopro_aliyun_express\command\InstallFull',