文件
xionggeshop/addons/shopro_wechat_trade/library/WxShippingPayload.php
可乐加冰 48cde5f891 ```
feat(shopro_huifu): 新增汇付个人进件自动提交功能

新增以下命令:
- php think shopro_huifu:onboard-auto-submit(每 5-10 分钟自动提交)
- php think shopro_huifu:onboard-poll(每 10 分钟轮询审核状态)
- php think shopro_huifu:diagnose-freeze-flow(冻结/解冻链路诊断)

完善了汇付插件的 README 文档,包含新功能说明和安装验证步骤。
```
2026-10-08 14:02:12 +08:00

169 行
5.9 KiB
PHP

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

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

<?php
namespace addons\shopro_wechat_trade\library;
use addons\shopro\model\OrderItem;
class WxShippingPayload
{
protected static $logisticsMap = [
'express' => 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']);
}
// 官方 POST /wxa/sec/order/notify_confirm_receive:received_time 为秒级时间戳(number),非 RFC3339
$payload = [
'transaction_id' => $transactionId,
'received_time' => intval($confirmTime),
];
$mchid = WxOrderContext::resolveMchid($order);
if ($mchid !== '') {
$payload['merchant_id'] = $mchid;
}
$orderSn = trim((string) ($order['order_sn'] ?? ''));
if ($orderSn !== '') {
$payload['merchant_trade_no'] = $orderSn;
}
return $payload;
}
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;
}
}