0
0
0

简单商城单商户小程序系统源码

职场区 3253582893 1月前
1313

源码获取:ym7k.com/18979/

简单商城单商户小程序系统:从电子面单到发货单的打印工程实现

引子:打印是商城系统最容易被忽视的工程环节

在商城系统的功能清单中,"电子面单打印"和"发货单打印"往往被放在最末位。但对于实际经营的商家来说,这两个功能的价值远超它们的排位。一个日处理 500 单的商家,如果每单都要手动填写快递单,需要 2-3 个专职人员;如果使用电子面单批量打印,一个人 10 分钟就能完成全部工作。

更关键的是,打印环节连接着商城系统和物流系统。它是订单从"已支付"变为"已发货"的物理动作,是数据世界和物理世界的交界点。这个交界点的工程可靠性,直接决定了商家的发货效率和错发漏发率。

本文以简单商城单商户小程序系统为例,从电子面单的对接原理、批量打印的并发控制、发货单的模板引擎、打印机的适配策略,到异常处理与重打机制,展开这类系统的完整技术分析。

一、电子面单的对接原理

1.1 什么是电子面单

电子面单(Electronic Waybill)是快递公司提供的数字化运单。商家通过 API 向快递公司申请运单号,获取面单的打印数据(通常是 PDF 或图片),然后通过热敏打印机打印出来,贴在包裹上。

电子面单的核心优势是:运单号由 API 自动分配,不需要手动填写;面单格式由快递公司统一,不需要商家设计;打印数据可以批量获取,支持高效率发货。

1.2 电子面单的技术流程

电子面单的对接涉及三方:商城系统、快递公司的开放平台、打印终端。完整流程如下:

text
商城系统 → 快递公司API → 获取运单号和面单数据
    ↓
商城系统 → 保存运单号到订单 → 生成打印任务
    ↓
打印终端 → 从商城系统获取面单数据 → 调用打印机打印

每一步都有工程细节需要处理。商城系统需要处理快递公司的 API 认证、参数格式、错误码;打印终端需要处理面单数据的格式转换、打印机指令的生成。

1.3 主流快递公司的 API 差异

国内主流快递公司(顺丰、中通、圆通、韵达、申通、极兔)都提供了电子面单 API,但接口设计差异很大。

顺丰 使用丰桥平台,需要企业认证和月结账号,API 风格偏企业级,参数复杂但功能完整。

中通、圆通、韵达、申通 使用菜鸟电子面单或各自的开放平台。菜鸟电子面单的优势是统一接口,一次对接支持多家快递。

极兔 提供独立的开放平台 API,接口相对简洁。

对于单商户商城系统,最务实的方案是对接菜鸟电子面单,一次对接覆盖主流快递公司。如果商家只用某一家快递(如顺丰),则直接对接该快递的 API。

php
<?php
// app/common/service/ExpressService.php

namespace app\common\service;

use think\facade\Db;
use think\facade\Log;

class ExpressService
{
    /**
     * 电子面单打印配置
     */
    protected $config = [
        'cainiao' => [
            'app_key' => '',
            'app_secret' => '',
            'cp_code' => '',  // 快递公司编码
            'template_url' => '',  // 面单模板URL
        ],
        'shunfeng' => [
            'partner_id' => '',
            'checkword' => '',
            'monthly_account' => '',
        ],
    ];

    /**
     * 获取电子面单
     */
    public function getWaybill(array $order): array
    {
        $provider = $order['express_provider'] ?? 'cainiao';

        switch ($provider) {
            case 'cainiao':
                return $this->getCainiaoWaybill($order);
            case 'shunfeng':
                return $this->getShunfengWaybill($order);
            default:
                throw new \Exception('不支持的快递服务商');
        }
    }

    /**
     * 获取菜鸟电子面单
     */
    protected function getCainiaoWaybill(array $order): array
    {
        $config = $this->config['cainiao'];

        // 构建请求参数
        $params = [
            'cp_code' => $config['cp_code'],
            'trade_order_info_dtos' => json_encode([[
                'object_id' => $order['order_no'],
                'logistics_id' => $order['express_company_code'],
                'sender' => [
                    'name' => $order['sender_name'],
                    'mobile' => $order['sender_phone'],
                    'province' => $order['sender_province'],
                    'city' => $order['sender_city'],
                    'district' => $order['sender_district'],
                    'detail' => $order['sender_address'],
                ],
                'receiver' => [
                    'name' => $order['receiver_name'],
                    'mobile' => $order['receiver_phone'],
                    'province' => $order['receiver_province'],
                    'city' => $order['receiver_city'],
                    'district' => $order['receiver_district'],
                    'detail' => $order['receiver_address'],
                ],
                'package_info' => [
                    'goods_name' => $order['goods_name'],
                    'count' => $order['goods_count'],
                    'weight' => $order['weight'] ?? 1,
                ],
            ]]),
        ];

        // 调用菜鸟 API
        $result = $this->callCainiaoApi('waybill', $params);

        if (empty($result['waybill_code'])) {
            throw new \Exception('获取面单失败: ' . ($result['message'] ?? '未知错误'));
        }

        return [
            'waybill_code' => $result['waybill_code'],
            'print_data' => $result['print_data'],
            'template_url' => $result['template_url'] ?? $config['template_url'],
        ];
    }

    /**
     * 调用菜鸟 API
     */
    protected function callCainiaoApi(string $method, array $params): array
    {
        $config = $this->config['cainiao'];

        $params['app_key'] = $config['app_key'];
        $params['timestamp'] = date('Y-m-d H:i:s');
        $params['format'] = 'json';
        $params['v'] = '1.0';
        $params['method'] = 'cainiao.waybill.' . $method;

        // 生成签名
        ksort($params);
        $signStr = $config['app_secret'];
        foreach ($params as $k => $v) {
            $signStr .= $k . $v;
        }
        $signStr .= $config['app_secret'];
        $params['sign'] = strtoupper(md5($signStr));

        // 发送请求
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => 'https://link.cainiao.com/gateway/link.do',
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($params),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
        ]);
        $response = curl_exec($ch);
        curl_close($ch);

        $result = json_decode($response, true);

        if (empty($result) || ($result['success'] ?? false) !== true) {
            Log::error('菜鸟面单API调用失败', [
                'method' => $method,
                'params' => $params,
                'response' => $response,
            ]);
            throw new \Exception('快递API调用失败');
        }

        return $result;
    }
}

这个服务的核心工程决策是将面单获取抽象为统一接口,具体实现按快递服务商分发。无论对接菜鸟还是顺丰,业务代码都调用 getWaybill($order),返回统一的 waybill_code 和 print_data。

二、批量打印的并发控制

2.1 批量获取面单

批量打印的核心是批量获取面单。如果一个订单一个订单地调用快递 API,100 个订单需要 100 次 HTTP 请求,耗时可能超过 30 秒。批量 API 允许一次请求获取多个订单的面单。

php
<?php
// app/common/service/BatchWaybillService.php

namespace app\common\service;

use think\facade\Db;
use think\facade\Queue;

class BatchWaybillService
{
    /**
     * 批量获取面单
     */
    public function batchGet(array $orderIds): array
    {
        $orders = Db::name('order')
            ->whereIn('id', $orderIds)
            ->where('status', 'paid')
            ->where('waybill_code', null)
            ->select()
            ->toArray();

        if (empty($orders)) {
            throw new \Exception('没有可打印的订单');
        }

        // 按快递公司分组(不同快递公司的 API 不同)
        $grouped = [];
        foreach ($orders as $order) {
            $provider = $order['express_provider'] ?? 'cainiao';
            $companyCode = $order['express_company_code'] ?? 'default';
            $key = $provider . '_' . $companyCode;
            $grouped[$key][] = $order;
        }

        $results = [
            'success' => [],
            'failed' => [],
        ];

        // 逐组批量获取
        foreach ($grouped as $key => $groupOrders) {
            [$provider, $companyCode] = explode('_', $key, 2);

            // 分批处理,每批最多 50 个订单(API 限制)
            $batches = array_chunk($groupOrders, 50);

            foreach ($batches as $batch) {
                try {
                    $waybills = $this->getBatchWaybill($provider, $batch);
                    foreach ($waybills as $orderId => $waybill) {
                        $results['success'][$orderId] = $waybill;
                    }
                } catch (\Exception $e) {
                    // 整批失败,记录失败原因
                    foreach ($batch as $order) {
                        $results['failed'][$order['id']] = [
                            'order_no' => $order['order_no'],
                            'reason' => $e->getMessage(),
                        ];
                    }
                }
            }
        }

        // 保存成功的面单到订单
        $this->saveWaybills($results['success']);

        return $results;
    }

    /**
     * 批量获取面单(单个快递公司)
     */
    protected function getBatchWaybill(string $provider, array $orders): array
    {
        $service = new ExpressService();

        $tradeOrders = [];
        foreach ($orders as $order) {
            $tradeOrders[] = [
                'object_id' => $order['order_no'],
                'logistics_id' => $order['express_company_code'],
                'sender' => [
                    'name' => $order['sender_name'],
                    'mobile' => $order['sender_phone'],
                    'province' => $order['sender_province'],
                    'city' => $order['sender_city'],
                    'district' => $order['sender_district'],
                    'detail' => $order['sender_address'],
                ],
                'receiver' => [
                    'name' => $order['receiver_name'],
                    'mobile' => $order['receiver_phone'],
                    'province' => $order['receiver_province'],
                    'city' => $order['receiver_city'],
                    'district' => $order['receiver_district'],
                    'detail' => $order['receiver_address'],
                ],
                'package_info' => [
                    'goods_name' => $order['goods_name'],
                    'count' => $order['goods_count'],
                    'weight' => $order['weight'] ?? 1,
                ],
            ];
        }

        // 调用批量 API
        $result = $service->callBatchApi($provider, $tradeOrders);

        $waybills = [];
        foreach ($result as $item) {
            if (!empty($item['waybill_code'])) {
                $orderId = $this->findOrderIdByNo($orders, $item['object_id']);
                if ($orderId) {
                    $waybills[$orderId] = [
                        'waybill_code' => $item['waybill_code'],
                        'print_data' => $item['print_data'],
                    ];
                }
            }
        }

        return $waybills;
    }

    /**
     * 保存面单到订单
     */
    protected function saveWaybills(array $waybills): void
    {
        Db::startTrans();
        try {
            foreach ($waybills as $orderId => $waybill) {
                Db::name('order')
                    ->where('id', $orderId)
                    ->update([
                        'waybill_code' => $waybill['waybill_code'],
                        'waybill_print_data' => $waybill['print_data'],
                        'waybill_at' => date('Y-m-d H:i:s'),
                    ]);
            }
            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            throw $e;
        }
    }

    protected function findOrderIdByNo(array $orders, string $orderNo): ?int
    {
        foreach ($orders as $order) {
            if ($order['order_no'] === $orderNo) {
                return $order['id'];
            }
        }
        return null;
    }
}

2.2 打印任务的队列化

批量打印不应该在前端请求中同步执行,因为快递 API 的响应时间不确定。正确的做法是将打印任务放入队列,前端轮询任务状态。

php
<?php
// app/job/PrintWaybillJob.php

namespace app\job;

use think\queue\Job;
use app\common\service\BatchWaybillService;

class PrintWaybillJob
{
    public function fire(Job $job, $data)
    {
        $taskId = $data['task_id'];
        $orderIds = $data['order_ids'];

        try {
            // 更新任务状态
            $this->updateTask($taskId, 'processing', 0);

            $service = new BatchWaybillService();
            $result = $service->batchGet($orderIds);

            // 更新任务结果
            $this->updateTask($taskId, 'completed', 100, [
                'success_count' => count($result['success']),
                'failed_count' => count($result['failed']),
                'failed_orders' => $result['failed'],
            ]);

            $job->delete();

        } catch (\Exception $e) {
            $this->updateTask($taskId, 'failed', 0, [
                'error' => $e->getMessage(),
            ]);
            \think\facade\Log::error('批量打印任务失败', [
                'task_id' => $taskId,
                'error' => $e->getMessage(),
            ]);
            $job->delete();
        }
    }

    protected function updateTask(int $taskId, string $status, int $progress, array $extra = []): void
    {
        Db::name('print_task')
            ->where('id', $taskId)
            ->update(array_merge([
                'status' => $status,
                'progress' => $progress,
                'updated_at' => date('Y-m-d H:i:s'),
            ], $extra));
    }
}

打印任务的表结构:

sql
CREATE TABLE `print_task` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `task_no` varchar(32) NOT NULL,
  `type` tinyint(1) NOT NULL COMMENT '1=电子面单 2=发货单',
  `order_ids` text NOT NULL COMMENT '订单ID列表JSON',
  `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/processing/completed/failed',
  `progress` int(11) NOT NULL DEFAULT '0',
  `success_count` int(11) NOT NULL DEFAULT '0',
  `failed_count` int(11) NOT NULL DEFAULT '0',
  `failed_orders` text COMMENT '失败订单详情JSON',
  `error` varchar(500) DEFAULT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_task_no` (`task_no`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

任务表的存在使打印过程可追溯。 商家可以查看每个打印任务的执行状态、成功数量、失败原因。如果某个订单打印失败,可以单独重试,而不需要重新提交整个批次。

三、发货单的模板引擎

3.1 发货单与电子面单的区别

电子面单是给快递公司用的,格式由快递公司决定。发货单是给商家和收件人用的,格式由商家自己决定。发货单通常包含:订单号、商品清单、收件人信息、商家信息、备注。

发货单的工程挑战是模板的灵活性。不同商家对发货单的格式要求不同:有的需要打印商品图片,有的只需要文字;有的需要打印价格,有的不需要。系统需要提供模板配置能力,让商家自定义发货单的布局。

3.2 模板引擎的设计

发货单模板采用 HTML + CSS 描述,通过模板引擎渲染为 HTML,再由打印组件转换为打印指令。

php
<?php
// app/common/service/InvoiceService.php

namespace app\common\service;

use think\facade\Db;
use think\facade\View;

class InvoiceService
{
    /**
     * 生成发货单 HTML
     */
    public function generateHtml(int $orderId, array $template = []): string
    {
        $order = $this->getOrderWithItems($orderId);

        // 默认模板
        if (empty($template)) {
            $template = $this->getDefaultTemplate();
        }

        // 渲染模板
        $html = $this->renderTemplate($template, $order);

        return $html;
    }

    /**
     * 获取订单及商品明细
     */
    protected function getOrderWithItems(int $orderId): array
    {
        $order = Db::name('order')->where('id', $orderId)->find();
        $items = Db::name('order_item')->where('order_id', $orderId)->select()->toArray();

        $order['items'] = $items;
        $order['total_quantity'] = array_sum(array_column($items, 'quantity'));
        $order['created_at_formatted'] = date('Y-m-d H:i', strtotime($order['created_at']));

        return $order;
    }

    /**
     * 渲染模板
     */
    protected function renderTemplate(array $template, array $order): string
    {
        $html = '<!DOCTYPE html><html><head><meta charset="utf-8">';
        $html .= '<style>' . $this->getBaseStyles($template) . '</style>';
        $html .= '</head><body>';

        // 头部
        if ($template['show_header'] ?? true) {
            $html .= '<div class="header">';
            $html .= '<h1>' . htmlspecialchars($template['title'] ?? '发货单') . '</h1>';
            $html .= '</div>';
        }

        // 订单信息
        $html .= '<div class="order-info">';
        $html .= '<div class="row"><span>订单号:</span><span>' . $order['order_no'] . '</span></div>';
        $html .= '<div class="row"><span>下单时间:</span><span>' . $order['created_at_formatted'] . '</span></div>';
        $html .= '<div class="row"><span>收件人:</span><span>' . $order['receiver_name'] . '</span></div>';
        $html .= '<div class="row"><span>电话:</span><span>' . $order['receiver_phone'] . '</span></div>';
        $html .= '<div class="row"><span>地址:</span><span>' . $order['receiver_province'] 
               . $order['receiver_city'] . $order['receiver_district'] 
               . $order['receiver_address'] . '</span></div>';
        $html .= '</div>';

        // 商品明细
        $html .= '<table class="items">';
        $html .= '<thead><tr>';
        $html .= '<th>商品名称</th>';
        $html .= '<th>规格</th>';
        $html .= '<th>数量</th>';
        if ($template['show_price'] ?? true) {
            $html .= '<th>单价</th>';
        }
        $html .= '</tr></thead><tbody>';

        foreach ($order['items'] as $item) {
            $html .= '<tr>';
            $html .= '<td>' . htmlspecialchars($item['product_name']) . '</td>';
            $html .= '<td>' . htmlspecialchars($item['spec_text'] ?? '') . '</td>';
            $html .= '<td>' . $item['quantity'] . '</td>';
            if ($template['show_price'] ?? true) {
                $html .= '<td>¥' . $item['price'] . '</td>';
            }
            $html .= '</tr>';
        }

        $html .= '</tbody></table>';

        // 底部
        $html .= '<div class="footer">';
        $html .= '<div>商品总数:' . $order['total_quantity'] . '</div>';
        if ($template['show_price'] ?? true) {
            $html .= '<div>订单金额:¥' . $order['pay_amount'] . '</div>';
        }
        if (!empty($order['remark'])) {
            $html .= '<div>备注:' . htmlspecialchars($order['remark']) . '</div>';
        }
        $html .= '</div>';

        $html .= '</body></html>';

        return $html;
    }

    /**
     * 默认模板配置
     */
    protected function getDefaultTemplate(): array
    {
        return [
            'title' => '发货单',
            'show_header' => true,
            'show_price' => true,
            'paper_width' => 100,  // mm
            'font_size' => 12,
        ];
    }

    /**
     * 基础样式
     */
    protected function getBaseStyles(array $template): string
    {
        $fontSize = $template['font_size'] ?? 12;
        $paperWidth = $template['paper_width'] ?? 100;

        return "
            body { font-family: 'SimSun', sans-serif; font-size: {$fontSize}px; margin: 0; padding: 5mm; width: {$paperWidth}mm; }
            .header { text-align: center; border-bottom: 1px solid #000; padding-bottom: 3mm; margin-bottom: 3mm; }
            .header h1 { font-size: 18px; margin: 0; }
            .order-info { margin-bottom: 3mm; }
            .order-info .row { display: flex; margin-bottom: 1mm; }
            .order-info .row span:first-child { width: 20mm; font-weight: bold; }
            table.items { width: 100%; border-collapse: collapse; margin-bottom: 3mm; }
            table.items th, table.items td { border: 1px solid #000; padding: 1mm; text-align: left; }
            table.items th { background: #f0f0f0; }
            .footer { border-top: 1px solid #000; padding-top: 3mm; }
            .footer div { margin-bottom: 1mm; }
        ";
    }
}

3.3 模板的自定义配置

商家可以在后台自定义发货单模板。配置存储在数据库中,商家可以调整:

sql
CREATE TABLE `invoice_template` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `config` text NOT NULL COMMENT '模板配置JSON',
  `is_default` tinyint(1) NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

配置示例:

json
{
  "title": "XX商城发货单",
  "show_header": true,
  "show_price": true,
  "paper_width": 100,
  "font_size": 12,
  "show_product_image": false,
  "show_remark": true,
  "footer_text": "感谢您的惠顾,如有问题请联系客服"
}

四、打印机适配与打印指令

4.1 打印机的类型

商城系统涉及的打印机主要有两类。

热敏打印机。 用于打印电子面单,通常是 100mm × 150mm 的规格。通过 USB、蓝牙或网络连接。主流品牌包括汉印、芝柯、得力。

针式打印机或激光打印机。 用于打印发货单,通常是 A4 或 A5 纸张。通过 USB 或网络连接。

4.2 小程序端的打印方案

小程序端无法直接调用系统打印机,需要通过以下方案之一。

方案一:云打印。 打印机连接到云打印服务(如易联云、飞鹅云),小程序通过 API 向云打印服务发送打印任务。优点是无需本地驱动,支持远程打印;缺点是需要额外购买云打印机。

方案二:蓝牙打印。 小程序通过蓝牙连接热敏打印机,发送 ESC/POS 指令。优点是不依赖网络,适合移动场景;缺点是需要处理蓝牙连接和指令生成。

方案三:导出 PDF 后手动打印。 小程序生成 PDF 文件,用户下载后通过其他应用打开并打印。优点是实现简单;缺点是操作步骤多,效率低。

对于单商户商城系统,云打印是最务实的方案。商家购买一台云打印机(成本约 200-500 元),系统通过 API 发送打印任务,打印机自动打印。

php
<?php
// app/common/service/CloudPrintService.php

namespace app\common\service;

use think\facade\Log;

class CloudPrintService
{
    protected $apiUrl = 'https://open.10ss.net:8888';

    /**
     * 发送打印任务到云打印服务
     */
    public function print(string $machineCode, string $content, int $copies = 1): bool
    {
        // 生成打印内容(ESC/POS 指令)
        $printContent = $this->buildPrintContent($content, $copies);

        // 调用云打印 API
        $params = [
            'machine_code' => $machineCode,
            'content' => $printContent,
            'origin_id' => uniqid('print_', true),
        ];

        $result = $this->callApi('/api/print', $params);

        if (($result['error'] ?? 1) !== 0) {
            Log::error('云打印失败', [
                'machine_code' => $machineCode,
                'result' => $result,
            ]);
            return false;
        }

        return true;
    }

    /**
     * 构建打印内容
     */
    protected function buildPrintContent(string $content, int $copies): string
    {
        // ESC/POS 指令
        $commands = [];

        // 初始化
        $commands[] = chr(27) . '@';

        // 打印份数
        for ($i = 0; $i < $copies; $i++) {
            $commands[] = $content;
            $commands[] = "\n\n\n";
        }

        // 切纸
        $commands[] = chr(29) . 'V' . chr(66) . chr(0);

        return implode('', $commands);
    }

    protected function callApi(string $path, array $params): array
    {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $this->apiUrl . $path,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($params),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
        ]);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true) ?: [];
    }
}

五、异常处理与重打机制

5.1 打印失败的常见原因

打印失败的原因可以分为三类。

API 层失败。 快递公司 API 超时、返回错误码、配额耗尽。这类失败可以通过重试解决。

数据层失败。 收件人地址不完整、手机号格式错误、商品信息缺失。这类失败需要修正数据后重试。

打印层失败。 打印机离线、纸张不足、连接中断。这类失败需要在打印终端处理。

5.2 重打机制

重打是打印功能的重要保障。商家需要能够对失败的订单单独重打,而不需要重新提交整个批次。

php
<?php
// app/common/service/ReprintService.php

namespace app\common\service;

use think\facade\Db;

class ReprintService
{
    /**
     * 重打单个订单的面单
     */
    public function reprintWaybill(int $orderId): array
    {
        $order = Db::name('order')->where('id', $orderId)->find();

        if (!$order) {
            throw new \Exception('订单不存在');
        }

        // 如果已有面单号,直接使用
        if (!empty($order['waybill_code']) && !empty($order['waybill_print_data'])) {
            return [
                'waybill_code' => $order['waybill_code'],
                'print_data' => $order['waybill_print_data'],
            ];
        }

        // 没有面单号,重新获取
        $service = new ExpressService();
        $waybill = $service->getWaybill($order);

        // 保存
        Db::name('order')
            ->where('id', $orderId)
            ->update([
                'waybill_code' => $waybill['waybill_code'],
                'waybill_print_data' => $waybill['print_data'],
                'waybill_at' => date('Y-m-d H:i:s'),
            ]);

        return $waybill;
    }

    /**
     * 批量重打
     */
    public function batchReprint(array $orderIds): array
    {
        $results = ['success' => [], 'failed' => []];

        foreach ($orderIds as $orderId) {
            try {
                $waybill = $this->reprintWaybill($orderId);
                $results['success'][$orderId] = $waybill;
            } catch (\Exception $e) {
                $results['failed'][$orderId] = $e->getMessage();
            }
        }

        return $results;
    }

    /**
     * 打印记录
     */
    public function logPrint(int $orderId, string $type, string $status, string $message = ''): void
    {
        Db::name('print_log')->insert([
            'order_id' => $orderId,
            'type' => $type,
            'status' => $status,
            'message' => $message,
            'created_at' => date('Y-m-d H:i:s'),
        ]);
    }
}

5.3 打印日志的价值

打印日志表记录了每次打印操作的时间、类型、状态和结果。它的工程价值有三点。

问题追溯。 当商家说"某个订单打印失败了",可以通过日志查到具体的失败原因和时间。

重打控制。 系统可以限制同一订单的重打次数,防止恶意重打或误操作。

统计分析。 打印成功率、平均打印耗时等指标,可以帮助优化系统。

sql
CREATE TABLE `print_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_id` int(11) NOT NULL,
  `type` tinyint(1) NOT NULL COMMENT '1=电子面单 2=发货单',
  `status` tinyint(1) NOT NULL COMMENT '1=成功 0=失败',
  `message` varchar(500) DEFAULT NULL,
  `operator_id` int(11) DEFAULT NULL COMMENT '操作人',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_order` (`order_id`),
  KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

六、边界与张力

简单商城单商户小程序系统的打印功能,在工程实践中,有几个需要清醒认识的边界。

第一,快递 API 的稳定性和配额。 快递公司的 API 不是 100% 可用的。网络抖动、系统维护、配额耗尽都可能导致调用失败。系统需要实现重试机制、降级方案(如切换到备用快递)和配额监控。

第二,面单模板的地区差异。 不同地区的快递公司可能使用不同的面单模板。同一个快递公司在不同省份的模板也可能不同。系统需要支持模板的按地区配置,或者对接快递公司的模板管理 API。

第三,小程序的打印能力限制。 微信小程序无法直接调用系统打印机,必须依赖云打印服务或蓝牙打印。这增加了硬件成本(云打印机)和开发复杂度(蓝牙指令生成)。

第四,隐私数据的处理。 电子面单包含收件人的姓名、电话、地址,这些是敏感个人信息。系统在打印数据存储和传输时需要加密,且打印完成后应及时清理不必要的缓存。

第五,批量打印的并发限制。 快递 API 通常有 QPS 限制(如每秒 10 次)。批量打印 500 个订单时,如果并发发送请求,会触发限流。系统需要实现请求队列和速率控制。

结语:打印是商城系统"最后一公里"的工程保障

简单商城单商户小程序系统的打印功能,其工程本质可以归结为一句话:它是订单从数据世界变为物理包裹的最后一公里。

电子面单的对接涉及快递 API 的认证、签名、参数格式和错误处理。批量打印涉及并发控制、队列化和任务状态管理。发货单涉及模板引擎和打印指令生成。异常处理涉及重打机制和打印日志。

这些技术细节不会出现在产品宣传页上,但它们的可靠性直接决定了商家的发货效率。一个打印功能不稳定的商城系统,即使其他功能再完善,也无法支撑实际的电商运营。

对于单商户商城系统,打印功能的工程目标是:商家在后台勾选订单,点击"批量打印",30 秒内完成 100 个订单的面单打印,且失败订单有清晰的重打入口。 这个目标看似简单,但实现它需要处理快递 API 对接、并发控制、打印指令生成、异常处理等一系列工程问题。这才是"支持电子面单打印、发货单打印"这句话背后的真正技术含量。

最新回复 (0)

    暂无评论

请先登录后发表评论!

返回
请先登录后发表评论!