文件
xionggeshop/addons/shopro_wechat_trade/library/WxShippingService.php
可乐加冰 66641f8a05 ```
feat(shopro_wechat_trade): 添加重新同步物流信息命令

新增 ResyncShipping 命令,支持强制重新上传微信物流信息,
不依赖 sync 表的 failed/pending 状态,适用于修改自提配置后
需要重传的场景。

同时修改 RetryFailed 命令,在没有待重试记录时提供操作提示,
并扩展 WxShippingService 的 retryFailed 方法支持处理
failed 状态的记录。
```
2026-10-09 13:01:01 +08:00

202 行
6.6 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace addons\shopro_wechat_trade\library;
use addons\shopro\model\Order;
use addons\shopro\model\OrderItem;
use think\Db;
use think\Log;
class WxShippingService
{
protected $client;
public function __construct()
{
$this->client = new WxTradeClient();
}
public function syncOrderShipping($orderId, $triggerItemId = 0)
{
if (!Bridge::isEnabled()) {
return false;
}
$order = Order::with('item')->where('id', $orderId)->find();
if (!$order || !Bridge::shouldHandleOrder($order)) {
return false;
}
$items = [];
if (!empty($order->item)) {
foreach ($order->item as $item) {
$items[] = is_object($item) && method_exists($item, 'toArray') ? $item->toArray() : (array) $item;
}
}
$triggerItem = null;
if ($triggerItemId) {
foreach ($items as $item) {
$itemArr = is_object($item) ? $item->toArray() : $item;
if (intval($itemArr['id']) === intval($triggerItemId)) {
$triggerItem = $itemArr;
break;
}
}
}
$built = WxShippingPayload::buildUploadPayload($order, $items, $triggerItem);
$payload = $built['payload'];
$logId = SyncLogger::create($order, 'shipping', $payload, $triggerItemId, $built['meta']['delivery_mode']);
try {
$response = $this->client->postJson('/wxa/sec/order/upload_shipping_info', $payload);
$this->client->callOrThrow($response, '上传发货信息');
SyncLogger::markSuccess($logId, $response, $payload);
return true;
} catch (\Exception $e) {
$response = isset($response) && is_array($response) ? $response : [];
SyncLogger::markFailed($logId, $e, $response, $payload);
Log::error('[shopro_wechat_trade] shipping sync fail order#' . $orderId . ': ' . $e->getMessage());
throw $e;
}
}
public function syncOrderConfirm($orderId)
{
if (!Bridge::isEnabled()) {
return false;
}
if (SyncLogger::hasSuccessfulConfirm($orderId)) {
return true;
}
if (!SyncLogger::hasSuccessfulShipping($orderId)) {
throw new \RuntimeException('须先成功 upload_shipping_info 再同步确认收货(notify_confirm_receive)');
}
$order = Order::with('item')->where('id', $orderId)->find();
if (!$order || !Bridge::shouldHandleOrder($order)) {
return false;
}
$pending = OrderItem::where('order_id', $orderId)
->where('dispatch_status', '<>', OrderItem::DISPATCH_STATUS_GETED)
->count();
if ($pending > 0) {
return false;
}
$payload = WxShippingPayload::buildConfirmPayload($order);
$logId = SyncLogger::create($order, 'confirm', $payload);
try {
$response = $this->client->postJson('/wxa/sec/order/notify_confirm_receive', $payload);
$this->client->callOrThrow($response, '同步确认收货');
SyncLogger::markSuccess($logId, $response, $payload);
return true;
} catch (\Exception $e) {
$response = isset($response) && is_array($response) ? $response : [];
SyncLogger::markFailed($logId, $e, $response, $payload);
Log::error('[shopro_wechat_trade] confirm sync fail order#' . $orderId . ': ' . $e->getMessage());
throw $e;
}
}
public function backfillShippedOrders($limit = 50)
{
$rows = Db::name('shopro_order')
->alias('o')
->join('shopro_order_item oi', 'oi.order_id = o.id')
->where('o.platform', 'wxMiniProgram')
->where('o.pay_type', 'in', ['wechat', 'huifu_wechat'])
->where('o.status', '>', 0)
->where('oi.dispatch_status', '>=', OrderItem::DISPATCH_STATUS_SENDED)
->group('o.id')
->order('o.id', 'desc')
->limit($limit)
->column('o.id');
$count = 0;
foreach ($rows as $orderId) {
$hasSuccess = Db::name('shopro_wechat_trade_sync')
->where('order_id', $orderId)
->where('sync_type', 'shipping')
->where('status', 'success')
->find();
if ($hasSuccess) {
continue;
}
try {
$this->syncOrderShipping($orderId);
$count++;
} catch (\Exception $e) {
// logged
}
}
return $count;
}
public function retryFailed($limit = 50)
{
$now = time();
$rows = Db::name('shopro_wechat_trade_sync')
->where(function ($q) use ($now) {
$q->where(function ($q2) use ($now) {
$q2->where('status', 'pending')
->where('retry_count', '>', 0)
->where('next_retry_time', '<=', $now);
})->whereOr('status', 'failed');
})
->order('id', 'asc')
->limit($limit)
->select();
$count = 0;
foreach ($rows as $row) {
try {
if ($row['sync_type'] === 'confirm') {
$this->syncOrderConfirm($row['order_id']);
} else {
$this->syncOrderShipping($row['order_id'], $row['order_item_id']);
}
$count++;
} catch (\Exception $e) {
// logged
}
}
return $count;
}
/**
* 按订单号再次调用 upload_shipping_info(已有 success 也会重传,用于改自提等)
*/
public function resyncShippingByOrderSns(array $orderSns)
{
$ok = 0;
$fail = 0;
foreach ($orderSns as $sn) {
$sn = trim((string) $sn);
if ($sn === '') {
continue;
}
$order = Db::name('shopro_order')->where('order_sn', $sn)->find();
if (!$order && strlen($sn) < 24) {
$order = Db::name('shopro_order')->where('order_sn', 'like', $sn . '%')->order('id', 'desc')->find();
}
if (!$order) {
$fail++;
continue;
}
try {
$this->syncOrderShipping((int) $order['id']);
$ok++;
} catch (\Exception $e) {
$fail++;
}
}
return ['ok' => $ok, 'fail' => $fail];
}
}