1, 'store' => 2, 'autosend' => 3, 'selfetch' => 4, ]; public static function mapLogisticsType($dispatchType) { return self::$logisticsMap[$dispatchType] ?? 1; } public static function buildUploadPayload($order, array $items, $triggerItem = null) { $transactionId = WxOrderContext::resolveTransactionId($order); $openid = WxOrderContext::resolveOpenid($order); if ($transactionId === '') { throw new \RuntimeException('缺少微信 transaction_id,无法同步发货'); } if ($openid === '') { throw new \RuntimeException('缺少用户 openid,无法同步发货'); } $shippableItems = self::filterShippableItems($items); if (!$shippableItems) { throw new \RuntimeException('没有可同步的已发货商品'); } $allItems = self::filterAllActiveItems($items); $deliveryMode = count($shippableItems) >= count($allItems) ? 1 : 2; $logisticsType = self::mapLogisticsType($triggerItem['dispatch_type'] ?? ($shippableItems[0]['dispatch_type'] ?? 'express')); $shippingList = []; foreach ($shippableItems as $item) { $shippingList[] = self::buildShippingListItem($item, $order); } return [ 'payload' => [ 'order_key' => [ 'order_number_type' => 2, 'transaction_id' => $transactionId, ], 'logistics_type' => $logisticsType, 'delivery_mode' => $deliveryMode, 'shipping_list' => $shippingList, 'upload_time' => self::formatUploadTime(time()), 'payer' => ['openid' => $openid], ], 'meta' => [ 'transaction_id' => $transactionId, 'delivery_mode' => $deliveryMode, 'logistics_type' => $logisticsType, ], ]; } public static function buildConfirmPayload($order) { $transactionId = WxOrderContext::resolveTransactionId($order); if ($transactionId === '') { throw new \RuntimeException('缺少微信 transaction_id,无法同步确认收货'); } $confirmTime = time(); $ext = is_array($order['ext_arr'] ?? null) ? $order['ext_arr'] : []; if (!empty($ext['confirm_time'])) { $confirmTime = intval($ext['confirm_time']); } return [ 'order_key' => [ 'order_number_type' => 2, 'transaction_id' => $transactionId, ], 'received_time' => self::formatUploadTime($confirmTime), ]; } protected static function buildShippingListItem(array $item, $order) { $dispatchType = $item['dispatch_type'] ?? 'express'; $desc = mb_substr($item['goods_title'] ?? '商城商品', 0, 120); $entry = ['item_desc' => $desc]; if (in_array($dispatchType, ['express', 'store'], true)) { $trackingNo = trim((string) ($item['express_no'] ?? '')); $expressCode = WxExpressMapper::toWechatCode($item['express_code'] ?? ''); if ($trackingNo === '') { throw new \RuntimeException('快递发货缺少物流单号: item#' . ($item['id'] ?? '')); } if ($expressCode === '') { throw new \RuntimeException('快递公司编码未映射: ' . ($item['express_code'] ?? '')); } $entry['tracking_no'] = $trackingNo; $entry['express_company'] = $expressCode; } else { $entry['tracking_no'] = $order['order_sn'] ?? ('ITEM' . ($item['id'] ?? '')); } if (!empty($order['phone'])) { $entry['contact'] = [ 'receiver_contact' => self::maskPhone($order['phone']), ]; } return $entry; } protected static function filterShippableItems(array $items) { $result = []; foreach ($items as $item) { $item = is_object($item) && method_exists($item, 'toArray') ? $item->toArray() : (array) $item; if (intval($item['dispatch_status'] ?? 0) < OrderItem::DISPATCH_STATUS_SENDED) { continue; } if (in_array(intval($item['refund_status'] ?? 0), [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH], true)) { continue; } $result[] = $item; } return $result; } protected static function filterAllActiveItems(array $items) { $result = []; foreach ($items as $item) { $item = is_object($item) && method_exists($item, 'toArray') ? $item->toArray() : (array) $item; if (in_array(intval($item['refund_status'] ?? 0), [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH], true)) { continue; } $result[] = $item; } return $result; } protected static function formatUploadTime($timestamp) { $dt = new \DateTime('@' . intval($timestamp)); $dt->setTimezone(new \DateTimeZone('Asia/Shanghai')); return $dt->format('Y-m-d\TH:i:s.vP'); } protected static function maskPhone($phone) { $phone = trim((string) $phone); if (strlen($phone) >= 11) { return substr($phone, 0, 3) . '****' . substr($phone, -4); } return $phone; } }