$value) { if ($value !== '' && $value !== null) { $config[$key] = $value; } } } } catch (\Exception $e) { \think\Log::warning('读取后台标签打印配置失败: ' . $e->getMessage()); } $this->user = isset($config['client_id']) ? trim($config['client_id']) : ''; $this->ukey = isset($config['client_secret']) ? trim($config['client_secret']) : ''; $this->sn = isset($config['sn']) ? trim($config['sn']) : ''; $this->enabled = !isset($config['enable']) || (string)$config['enable'] !== '0'; $this->copies = isset($config['copies']) ? max(1, min(65535, (int)$config['copies'])) : 1; $this->expiresIn = isset($config['expires_in']) ? max(1, min(86399, (int)$config['expires_in'])) : 3600; $this->shopName = !empty($config['shop_name']) ? trim($config['shop_name']) : $this->shopName; $this->labelWidthMm = $this->normalizeMillimeters(isset($config['label_width_mm']) ? $config['label_width_mm'] : 55, 20, 120, 55); $this->labelHeightMm = $this->normalizeMillimeters(isset($config['label_height_mm']) ? $config['label_height_mm'] : 55, 20, 120, 55); $this->labelGapMm = $this->normalizeMillimeters(isset($config['label_gap_mm']) ? $config['label_gap_mm'] : 2, 0, 20, 2); } public function queryStatus() { return $this->sendRequest('queryPrinterStatus', ['sn' => $this->sn]); } public function queryOrderState($cloudOrderId) { $result = $this->sendRequest('queryOrderState', ['orderId' => (string)$cloudOrderId]); $result['printed'] = !empty($result['accepted']) && !empty($result['data']); return $result; } public function printOrder($orderData = [], $copies = null) { return $this->printLabelOrder($orderData, $copies); } public function printLabelOrder($orderData = [], $copies = null) { if (!$this->enabled) { return $this->failure('后台未启用标签打印'); } try { $content = $this->buildLabelContent($orderData); } catch (\InvalidArgumentException $e) { return $this->failure($e->getMessage()); } if ($this->gbkLength($content) > 12 * 1024) { return $this->failure('打印内容超过芯烨云 12KB 限制'); } $idempotent = isset($orderData['print_idempotent']) ? trim((string)$orderData['print_idempotent']) : ''; if ($idempotent !== '') { $idempotent = substr($idempotent, 0, 50); } $result = $this->sendRequest('printLabel', [ 'sn' => $this->sn, 'content' => $content, 'copies' => $copies === null ? $this->copies : max(1, min(65535, (int)$copies)), // 离线时由芯烨云暂存,避免本地重发造成重复标签。 'mode' => 1, 'expiresIn' => $this->expiresIn, 'idempotent' => $idempotent, ]); if (!empty($result['accepted']) && !is_scalar($result['data'])) { return $this->failure('芯烨云未返回打印订单号', $result['response']); } if (!empty($result['accepted'])) { $result['cloud_order_id'] = (string)$result['data']; } return $result; } public function buildLabelContent($order = []) { $widthDots = (int)round($this->labelWidthMm * 8); $heightDots = (int)round($this->labelHeightMm * 8); if ($widthDots < 440 || $heightDots < 440) { throw new \InvalidArgumentException('订单标签需要至少 55×55mm 的标签纸'); } $rows = []; $this->appendLabelField($rows, '订单:', isset($order['order_sn']) ? $order['order_sn'] : 'TEST' . date('YmdHis'), 1, '订单编号'); $this->appendLabelField($rows, '时间:', isset($order['createtime']) ? date('Y-m-d H:i:s', (int)$order['createtime']) : date('Y-m-d H:i:s'), 1, '下单时间'); $this->appendLabelField($rows, '店铺:', isset($order['buyer_shop_name']) && $order['buyer_shop_name'] !== '' ? $order['buyer_shop_name'] : '-', 1, '购买用户店铺名称'); $this->appendLabelField($rows, '商品:', $this->buildGoodsText(isset($order['items']) && is_array($order['items']) ? $order['items'] : []), 4, '商品名称和数量'); $this->appendLabelField($rows, '电话:', isset($order['phone']) && $order['phone'] !== '' ? $order['phone'] : '-', 1, '收货电话'); $this->appendLabelField($rows, '地址:', isset($order['address']) && $order['address'] !== '' ? $order['address'] : '到店自提', 3, '收货地址'); $this->appendLabelField($rows, '备注:', isset($order['memo']) && $order['memo'] !== '' ? $order['memo'] : '-', 3, '用户备注'); $content = ''; $content .= '' . $this->formatMillimeters($this->labelWidthMm) . ',' . $this->formatMillimeters($this->labelHeightMm) . ''; foreach ($rows as $index => $row) { $content .= '' . $row . ''; } return $content . ''; } protected function sendRequest($endpoint, array $data) { if (!$this->user || !$this->ukey || !$this->sn) { return $this->failure('打印机配置不完整'); } $timestamp = (string)time(); $payload = array_merge($data, [ 'user' => $this->user, 'timestamp' => $timestamp, 'sign' => sha1($this->user . $this->ukey . $timestamp), ]); $json = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($json === false) { return $this->failure('打印请求编码失败'); } $ch = curl_init($this->url . $endpoint); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $json); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json;charset=UTF-8', 'Accept: application/json']); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $body = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($error) { return $this->failure('云打印请求失败: ' . $error, '', true); } $response = trim((string)$body); $decoded = json_decode($response, true); $accepted = $httpCode >= 200 && $httpCode < 300 && is_array($decoded) && isset($decoded['code']) && (int)$decoded['code'] === 0; return [ 'accepted' => $accepted, 'data' => is_array($decoded) && array_key_exists('data', $decoded) ? $decoded['data'] : null, 'response' => mb_substr($response, 0, 1000, 'UTF-8'), 'error' => $accepted ? '' : (is_array($decoded) && !empty($decoded['msg']) ? $decoded['msg'] : '云打印未受理,HTTP ' . $httpCode), 'uncertain' => false, ]; } protected function failure($error, $response = '', $uncertain = false) { return ['accepted' => false, 'data' => null, 'response' => $response, 'error' => $error, 'uncertain' => $uncertain]; } protected function gbkLength($content) { $encoded = function_exists('mb_convert_encoding') ? @mb_convert_encoding($content, 'GBK', 'UTF-8') : @iconv('UTF-8', 'GBK//IGNORE', $content); return $encoded === false ? strlen($content) : strlen($encoded); } protected function cleanText($value) { return preg_replace('/[\r\n\t]+/', ' ', strip_tags((string)$value)); } protected function labelText($value, $maxWidth) { $text = $this->cleanText($value); if (function_exists('mb_strimwidth')) { $text = mb_strimwidth($text, 0, $maxWidth * 2, '', 'UTF-8'); } else { $text = substr($text, 0, $maxWidth * 2); } return str_replace(['<', '>'], ['<', '>'], $text); } protected function appendLabelField(array &$rows, $prefix, $value, $maxLines, $fieldName) { $lines = $this->splitLabelLines($value, 15, $maxLines, $fieldName); foreach ($lines as $index => $line) { $rows[] = ($index === 0 ? $prefix : '') . $this->labelText($line, 15); } } protected function buildGoodsText(array $items) { $goods = []; foreach ($items as $item) { $name = isset($item['name']) ? $this->cleanText($item['name']) : '商品'; $quantity = isset($item['num']) ? max(1, (int)$item['num']) : 1; $goods[] = $name . '×' . $quantity; } return $goods ? implode('、', $goods) : '无商品'; } protected function splitLabelLines($value, $lineWidth, $maxLines, $fieldName) { $text = $this->cleanText($value); $lines = []; $line = ''; $lineWidthUsed = 0; $length = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text); for ($index = 0; $index < $length; $index++) { $character = function_exists('mb_substr') ? mb_substr($text, $index, 1, 'UTF-8') : substr($text, $index, 1); $characterWidth = function_exists('mb_strwidth') ? mb_strwidth($character, 'UTF-8') : 1; if ($line !== '' && $lineWidthUsed + $characterWidth > $lineWidth * 2) { $lines[] = $line; $line = ''; $lineWidthUsed = 0; } $line .= $character; $lineWidthUsed += $characterWidth; } if ($line !== '' || empty($lines)) { $lines[] = $line ?: '-'; } if (count($lines) > $maxLines) { throw new \InvalidArgumentException($fieldName . '内容过长,无法放入单张 55×55mm 标签'); } return $lines; } protected function normalizeMillimeters($value, $min, $max, $default) { if (!is_numeric($value)) { return $default; } return max($min, min($max, (float)$value)); } protected function formatMillimeters($value) { return rtrim(rtrim(number_format((float)$value, 2, '.', ''), '0'), '.'); } }