当前位置:首页 > 技术 > 正文内容

物流系统集成:泸州老窖订单查询与创建实践

访客 技术 2026年7月9日 1

API 接口概览

为了实现与泸州老窖物流系统的无缝对接,我们定义了一系列内部 API 接口,用于查询运单信息及下达新订单。核心功能包括获取指定时间范围内的运单数据或通过特定运单号查询,并将这些数据同步到本地数据库。

运单查询接口

该接口允许通过日期范围或特定运单号查询泸州老窖的运单信息。如果未提供运单号,则必须提供起始和结束时间进行范围查询。

GET http://your-domain.com/api/logistics/lzlj/orders?startDate=2022-11-07 06:00:00&endDate=2022-11-08 06:00:00

下单 API 请求体示例

当通过 POST 请求下达新订单时,请求体需要包含发货方、收货方、货物信息以及其他辅助备注。以下是一个标准的 JSON 请求体结构:

{
	"authKey": "your_api_key",
	"authSecret": "your_api_secret",
	"signature": "generated_signature",
	"timestamp": 1666672744000,
	"orderPayload": {
		"recipient": {
			"name": "张三",
			"phone": "13888888888",
			"address": "广东省深圳市南山区金蝶软件园"
		},
		"sender": {
			"name": "李四",
			"phone": "13888888888",
			"address": "广东省深圳市南山区金蝶软件园"
		},
		"goodsDescription": "测试货物",
		"notes": "测试下单,请勿实际发货"
	}
}

路由配置

在 Laravel 框架中,接口的访问路径通过路由进行定义。为了支持上述功能,我们配置了以下路由规则:

  • `GET /api/logistics/lzlj/orders`: 用于查询泸州老窖运单数据。
  • `POST /api/logistics/lzlj/orders`: 用于向泸州老窖系统提交新订单。

这些路由指向了 `LogisticsIntegrationController` 中的相应方法。为确保接口安全,部分路由可能需要通过中间件进行认证,例如 `/api/console/logistics/lzlj/orders` 通常需要管理员权限。

// API 路由定义 (例如在 api.php)
$router->post('logistics/lzlj/orders', [LogisticsIntegrationController::class, 'handleOrderCreation']);
$router->get('logistics/lzlj/orders', [LogisticsIntegrationController::class, 'queryExternalOrders']);

// 后台管理路由 (例如在 console.php,通常带有认证中间件)
// $router->get('admin/logistics/lzlj/orders', [LogisticsIntegrationController::class, 'queryExternalOrders'])->middleware('auth:api');

控制器实现:订单处理逻辑

`LogisticsIntegrationController` 负责接收 HTTP 请求,验证参数,调度业务逻辑层处理数据,并将结果返回给客户端。以下是处理泸州老窖运单查询的核心方法示例:

use Illuminate\Http\Request;
use App\Services\Logistics\LzljLogisticsService; // 假设的服务层

class LogisticsIntegrationController extends BaseController
{
    /**
     * 查询泸州老窖订单并同步到本地数据库。
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function queryExternalOrders(Request $request)
    {
        try {
            // 获取请求参数:原始运单号、开始时间、结束时间
            $waybillNo = $request->input('waybill_no');
            $startTimeStr = $request->input('startDate');
            $endTimeStr = $request->input('endDate');

            $parsedStartTime = null;
            $parsedEndTime = null;

            // 参数校验:如果未提供运单号,则必须提供时间范围
            if (empty($waybillNo)) {
                if (empty($startTimeStr) || empty($endTimeStr)) {
                    return $this->errorResponse('查询泸州老窖订单,未提供运单号时必须指定开始和结束时间!', 400);
                }
                // 将时间字符串转换为 Unix 毫秒时间戳
                $parsedStartTime = strtotime($startTimeStr) * 1000;
                $parsedEndTime = strtotime($endTimeStr) * 1000;
            }

            // 调用物流服务层获取外部订单数据
            $externalResponse = LzljLogisticsService::fetchOrders($waybillNo, $parsedStartTime, $parsedEndTime);

            // 检查外部接口响应
            if (isset($externalResponse['RESULT']) && $externalResponse['RESULT'] === 'FAILURE') {
                return $this->errorResponse($externalResponse['MESSAGE'], 500);
            } else {
                // 格式化外部数据并准备插入本地数据库
                $recordsToInsert = LzljLogisticsService::formatExternalOrderData($externalResponse['DATA']);
                $newlyCreatedCount = 0;

                // 遍历格式化后的数据,并根据唯一哈希值创建或更新本地记录
                foreach ($recordsToInsert as $record) {
                    $localOrder = LogisticsOrder::firstOrCreate(
                        ['external_hash_id' => $record['external_hash_id']],
                        $record
                    );
                    if ($localOrder->wasRecentlyCreated) {
                        $newlyCreatedCount++;
                    }
                }
                // 返回新增的订单数量
                return $this->successResponse(['newly_created_orders' => $newlyCreatedCount]);
            }
        } catch (\Throwable $e) {
            \Log::error("查询泸州老窖订单失败: " . $e->getMessage(), ['exception' => $e]);
            return $this->errorResponse('查询订单过程中发生错误: ' . $e->getMessage(), 500);
        }
    }

    // handleOrderCreation 方法的示例,此处仅为占位
    public function handleOrderCreation(Request $request)
    {
        // ... 处理下单逻辑,调用 LzljLogisticsService::createOrder()
        return $this->successResponse(['message' => '订单创建请求已提交']);
    }
}

服务层:外部系统交互核心

`LzljLogisticsService` 封装了所有与泸州老窖外部系统交互的逻辑,包括环境配置、请求头生成、数据请求和响应处理。它充当了一个代理,将内部请求转换为外部系统可理解的格式,并将外部响应适配为内部系统所需的数据结构。

配置与初始化

服务层维护了不同环境下的外部 API 地址以及认证凭证等常量。通过构造函数或静态方法,可以根据当前运行环境加载相应的配置。

namespace App\Services\Logistics;

use App\Models\LogisticsOrder;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class LzljLogisticsService
{
    // 外部 API 地址配置
    const EXTERNAL_API_ENDPOINTS = [
        'development' => 'http://wwesbtest.lzlj.com/WP_LZLJ_SOA/APP_YZ_SERVICES/Proxy_Services/',
        'production' => 'http://wwesb.lzlj.com/WP_LZLJ_SOA/APP_YZ_SERVICES/Proxy_Services/', // 生产环境地址
    ];

    // 固定用户和公司 ID
    const DEFAULT_USER_ID = 10000;
    const DEFAULT_COMPANY_ID = 10000;

    // 紧急情况映射
    const EMERGENCY_LEVEL_MAP = [
        '常规' => 0,
        '紧急' => 1,
    ];

    // 当前环境的 API 基地址
    protected static $baseUrl;

    public function __construct()
    {
        // 根据 .env 文件中的 LZLJ_ENV 变量设置当前环境
        $environment = env('LZLJ_ENV', 'development');
        self::$baseUrl = self::EXTERNAL_API_ENDPOINTS[$environment];
    }

    /**
     * 获取当前环境的 API 基地址。
     *
     * @return string
     */
    public static function getBaseUrl()
    {
        if (!self::$baseUrl) {
            new static(); // 实例化以初始化 $baseUrl
        }
        return self::$baseUrl;
    }
}

运单查询接口实现

`fetchOrders` 方法负责构造查询请求,包括设置认证头和查询参数,并调用实际的 HTTP 客户端发送请求到泸州老窖系统。

    /**
     * 从泸州老窖系统获取运单信息。
     *
     * @param string|null $waybillNo 运单号
     * @param int|null $startDateMs 开始时间(毫秒时间戳)
     * @param int|null $endDateMs 结束时间(毫秒时间戳)
     * @param string|null $carrierKey 承运商密钥
     * @return array 外部系统响应数据
     */
    public static function fetchOrders($waybillNo = null, $startDateMs = null, $endDateMs = null, $carrierKey = null)
    {
        // 外部 API 路径
        $apiPath = 'TA_OSB/LZLJ_1143_YZ_OrderSynchronize_PS';
        $fullUrl = self::getBaseUrl() . $apiPath;

        $requestData = [];

        // 设置承运商密钥
        $requestData['CARRIER_KEY'] = $carrierKey ?? env('LZLJ_CARRIER_KEY', '10019033');

        // 设置查询参数
        if ($waybillNo) {
            $requestData['TRACK_NO'] = $waybillNo;
        }
        if ($startDateMs) {
            $requestData['START_DATE'] = $startDateMs;
        }
        if ($endDateMs) {
            $requestData['END_DATE'] = $endDateMs;
        }

        // 构建请求头
        $headers = self::buildRequestHeaders($requestData);

        // 构造最终的 POST 请求体
        $postBody = ['REQUEST' => ['HEAD' => $headers, 'LIST' => $requestData]];

        Log::info('请求泸州老窖运单接口: ' . $fullUrl, $postBody);

        // 调用通用 HTTP 客户端发送请求 (假设 glz() 函数在全局辅助函数中)
        // return glz($fullUrl, $postBody, 'POST');

        // 模拟外部接口响应,用于开发测试
        return json_decode(file_get_contents(app_path('../tests/Support/LZLJ_QUERY_MOCK.json')), true);
    }

注意:上述代码中的 `glz()` 函数假设是一个全局辅助函数,用于发送 HTTP 请求。在实际项目中,可能使用 Guzzle HTTP 客户端或其他 Laravel 内置的 `Http` facade。此处为保持与原文结构相似,采用了模拟数据文件替代实际的 `glz` 调用。

请求头构建

`buildRequestHeaders` 方法负责生成符合泸州老窖 API 要求的请求头,包括认证信息和时间戳。

    /**
     * 构建泸州老窖 API 请求头。
     *
     * @param array $data 请求数据负载
     * @param int|null $transactionCount 交易数量
     * @return array
     */
    public static function buildRequestHeaders(array $data, $transactionCount = null)
    {
        $currentDateTime = date('Y-m-d H:i:s');
        $headers = [
            "ACCOUNT" => env('LZLJ_API_ACCOUNT', "YD"),
            "PASSWORD" => null, // 密码将经过签名生成
            "CONSUMER" => env('LZLJ_API_CONSUMER', "YD"),
            "REQUESTDATE" => $currentDateTime,
            "BIZTRANSACTIONID" => "LZLJ_INTEGRATION_" . date('YmdHis') . Str::random(5), // 唯一业务交易ID
            "SRVLEVEL" => env('LZLJ_SRV_LEVEL', "OTB"),
        ];

        // 获取加密所需凭证
        $apiPassword = env('LZLJ_AUTH_PASSWORD', 'System1qaz*');
        $apiSalt = env('LZLJ_AUTH_SALT', '&%5123***&&%%$$#@');

        // 生成签名
        $signedPassword = md5($headers['ACCOUNT'] . $apiPassword . $currentDateTime . $apiSalt);
        $headers['PASSWORD'] = $signedPassword;

        // 如果提供了交易数量,则添加到头部
        if ($transactionCount !== null) {
            $headers['COUNT'] = $transactionCount;
        }

        return $headers;
    }

数据格式化

`formatExternalOrderData` 方法用于将从外部系统获取的原始订单数据转换为本地数据库模型所需的格式。这包括提取关键信息,填充默认值,并生成唯一标识。

    /**
     * 将泸州老窖的原始订单数据格式化为本地存储结构。
     *
     * @param array $externalDatas 从外部系统获取的原始数据数组
     * @return array 格式化后的订单记录数组
     */
    public static function formatExternalOrderData(array $externalDatas)
    {
        $defaultSenderInfo = [
            'company_id' => self::DEFAULT_COMPANY_ID,
            'user_id' => self::DEFAULT_USER_ID,
            'sender_name' => '泸州老窖销售有限公司',
            'sender_phone' => '0830-2752111',
            'sender_address' => '四川省泸州市龙马潭区鱼塘街道望江路11号',
            'sender_province' => '四川省',
            'sender_city' => '泸州市',
            'sender_district' => '龙马潭区',
        ];

        $formattedRecords = [];
        foreach ($externalDatas as $rawData) {
            $currentRecord = [];
            $currentRecord['original_payload'] = json_encode($rawData);
            $currentRecord['external_hash_id'] = md5($currentRecord['original_payload']); // 用于唯一识别外部记录
            $currentRecord['waybill_number'] = data_get($rawData, 'TRACK_NO');
            $currentRecord['order_sn'] = generateUniqueSystemNumber(LogisticsOrder::class, 'order_sn', 'LZO'); // 生成系统内部订单号
            $currentRecord['urgency_level'] = self::EMERGENCY_LEVEL_MAP[data_get($rawData, 'EMERGENCY')] ?? 0;

            $currentRecord['recipient_name'] = data_get($rawData, 'TONAME');
            $currentRecord['recipient_phone'] = data_get($rawData, 'TOMOBILE');
            $currentRecord['recipient_address'] = data_get($rawData, 'TOADDRESS');
            $currentRecord['recipient_province'] = data_get($rawData, 'TOPROVINCE');
            $currentRecord['recipient_city'] = data_get($rawData, 'TOCITY');
            $currentRecord['recipient_district'] = data_get($rawData, 'TOAREA');

            $currentRecord['cargo_description'] = data_get($rawData, 'DETAILS.ITEM_NAME');
            $currentRecord['payment_type'] = 0; // 假设默认0
            $currentRecord['insured_value'] = 0; // 假设默认0

            if (isset($rawData['PLAN_ARRIVAL_DATE']) && $rawData['PLAN_ARRIVAL_DATE']) {
                $currentRecord['expected_delivery_at'] = $rawData['PLAN_ARRIVAL_DATE'];
            }

            $currentRecord['sales_person_name'] = data_get($rawData, 'SALES_NAME');

            $formattedRecords[] = array_merge($defaultSenderInfo, $currentRecord);
        }
        return $formattedRecords;
    }

注意:`generateUniqueSystemNumber()` 函数是一个假设的辅助函数,用于生成唯一的订单序列号。`data_get()` 是 Laravel 提供的辅助函数,用于从数组或对象中安全地获取值。

状态反馈与同步

`syncOrderRouteStatus` 方法用于将本地订单的路由状态更新反馈给外部系统,例如签收、在途等。

    /**
     * 同步订单路由状态到外部系统。
     *
     * @return array 外部系统响应
     */
    public static function syncOrderRouteStatus()
    {
        $apiPath = 'TA_OSB/LZLJ_1144_YZ_BaseOrderFeedback_PS';
        $fullUrl = self::getBaseUrl() . $apiPath;

        // 定义需要反馈的状态数据结构
        $feedbackData = [
            "WAYBILL_NO" => "",      // 运单号
            "CURRENT_STATUS" => "",  // 当前状态码
            "STATUS_TIMESTAMP" => "",// 状态更新时间点
            "OPERATOR_NAME" => "",   // 操作人员
            "OPERATOR_TIME" => "",   // 操作时间
            "DETAIL_DESCRIPTION" => "", // 详细描述
            "RECEIVER_INFO" => "",   // 接收人信息
            "SMS_CODE" => "",        // 短信验证码
            "TRANSPORT_CODE" => "",  // 运输编码
            "TRANSPORT_NAME" => "",  // 运输名称
            "EXTENSION_FIELD1" => "",
            // ... 其他扩展字段
        ];

        // 获取单个记录的头部信息(COUNT=1)
        $headers = self::buildRequestHeaders($feedbackData, 1);

        Log::info('请求泸州老窖状态反馈接口: ' . $fullUrl, $feedbackData);

        // 调用通用 HTTP 客户端发送请求
        // return glz($fullUrl, $feedbackData, 'POST', $headers);
        return ['RESULT' => 'SUCCESS', 'MESSAGE' => '状态反馈成功']; // 模拟响应
    }

数据模型定义

在本地数据库中,我们定义了 `LogisticsOrder` 和 `SupplierPartner` 等模型来存储从外部系统同步的订单信息和相关数据。

`LogisticsOrder` 模型

`LogisticsOrder` 模型用于存储泸州老窖的运单详情,包括其原始数据、当前状态和相关关联关系。

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Models\BaseModel; // 假设有一个 Base Model
use Osi\LaravelControllerTrait\Models\FilterAndSorting; // 假设使用了 Osi 包

class LogisticsOrder extends BaseModel
{
    use HasFactory, FilterAndSorting;

    protected $guarded = []; // 允许批量赋值所有字段
    protected $hidden = ['original_payload']; // 隐藏原始数据字段,不默认返回

    // 订单状态枚举映射
    const INTERNAL_STATUS_MAP = [
        0 => 'PENDING_SHIPMENT',
        10 => 'SHIPPED',
        50 => 'IN_TRANSIT',
        90 => 'ARRIVED',
        100 => 'SIGNED',
        110 => 'RETURNED_TO_WAREHOUSE',
    ];

    const INTERNAL_STATUS_CN_MAP = [
        0 => '待发货',
        10 => '已发运',
        50 => '在途运输',
        90 => '已运抵',
        100 => '已签收',
        101 => '退件处理中',
        110 => '退件已入库',
    ];

    protected $casts = [
        'original_payload' => 'json', // 原始数据存储为 JSON 字符串
        'carrier_tracking_data' => 'json', // 承运商跟踪数据
    ];

    /**
     * 获取最新的路由更新记录。
     */
    public function latestRouteUpdate()
    {
        return $this->hasOne(OrderRouteUpdate::class)->latestOfMany();
    }

    /**
     * 获取所有路由更新记录。
     */
    public function routeUpdates()
    {
        return $this->hasMany(OrderRouteUpdate::class);
    }

    /**
     * 获取最新的打印日志。
     */
    public function latestPrintLog()
    {
        return $this->hasOne(OrderPrintLog::class)->latestOfMany();
    }

    /**
     * 获取所有打印日志。
     */
    public function printLogs()
    {
        return $this->hasMany(OrderPrintLog::class);
    }

    /**
     * 获取所有变更日志。
     */
    public function changeLogs()
    {
        return $this->hasMany(OrderChangeLog::class);
    }

    /**
     * 关联承运商信息。
     */
    public function carrier()
    {
        return $this->belongsTo(LogisticsCarrier::class);
    }

    /**
     * 尝试从外部系统取消订单。
     *
     * @param LogisticsOrder $order
     * @return bool
     */
    public static function attemptCancelFromExternal(LogisticsOrder $order): bool
    {
        // 假设订单状态大于50(例如已在途),则不允许取消
        if ($order->status_code > 50) {
            return false;
        }
        $order->status_code = 101; // 标记为退件处理中
        $order->save();
        return true;
    }
}

`SupplierPartner` 模型

`SupplierPartner` 模型用于管理与系统集成的供应商或合作伙伴信息,例如泸州老窖作为物流提供方。

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Models\BaseModel;
use Osi\LaravelControllerTrait\Models\FilterAndSorting;

class SupplierPartner extends BaseModel
{
    use HasFactory, FilterAndSorting;

    protected $guarded = [];
    protected $casts = [
        'configuration_options' => 'json', // 存储 JSON 配置选项
    ];

    // 可以定义与此供应商相关的订单、合同等关系
    public function logisticsOrders()
    {
        return $this->hasMany(LogisticsOrder::class, 'supplier_id');
    }
}

中间件:接口安全验证

为了确保与泸州老窖系统交互的安全性,我们引入了中间件对 API 请求进行身份验证和签名校验。`LzljApiAuthMiddleware` 负责在请求到达控制器之前,验证请求头中的 `authKey`、`authSecret`、`signature` 和 `timestamp`,以防止未授权访问和数据篡改。

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class LzljApiAuthMiddleware
{
    /**
     * 处理传入请求。
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        // 从请求头或请求体中获取认证参数
        $authKey = $request->header('X-LZLJ-AuthKey') ?? $request->input('authKey');
        $signature = $request->header('X-LZLJ-Signature') ?? $request->input('signature');
        $timestamp = $request->header('X-LZLJ-Timestamp') ?? $request->input('timestamp');
        $secret = config('logistics.lzlj.secrets.' . $authKey); // 从配置中获取对应的秘钥

        // 基本参数缺失检查
        if (empty($authKey) || empty($signature) || empty($timestamp) || empty($secret)) {
            return response()->json(['code' => 401, 'message' => '认证参数缺失或无效'], 401);
        }

        // 验证时间戳,防止重放攻击 (例如,允许5分钟内的误差)
        if (abs(time() - ($timestamp / 1000)) > 300) {
            return response()->json(['code' => 401, 'message' => '请求时间戳已过期或不正确'], 401);
        }

        // 重建待签名字符串
        // 这里需要根据泸州老窖实际的签名算法来构建字符串
        // 假设签名为 md5(authKey + secret + timestamp + JSON.stringify(orderPayload))
        $payloadToSign = $request->input('orderPayload'); // 获取原始请求体中的业务数据
        $stringToSign = $authKey . $secret . $timestamp . json_encode($payloadToSign, JSON_UNESCAPED_UNICODE);
        $expectedSignature = md5($stringToSign);

        if ($signature !== $expectedSignature) {
            \Log::warning('LZLJ API 签名验证失败', [
                'authKey' => $authKey,
                'receivedSignature' => $signature,
                'expectedSignature' => $expectedSignature,
                'stringToSign' => $stringToSign
            ]);
            return response()->json(['code' => 401, 'message' => '签名验证失败'], 401);
        }

        return $next($request);
    }
}

注意:具体的签名算法(`stringToSign` 的构建方式)应严格遵循泸州老窖提供的文档。上述示例仅为一种常见模式。

标签: Laravel

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。