文件
xiaozhongchuf/application/common/library/PrinterService.php
2026-09-23 17:04:10 +08:00

271 行
11 KiB
PHP

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

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

<?php
namespace app\common\library;
class PrinterService
{
protected $user;
protected $ukey;
protected $sn;
protected $enabled = true;
protected $copies = 1;
protected $expiresIn = 3600;
protected $shopName = '小众厨房设备商城';
protected $labelWidthMm = 55;
protected $labelHeightMm = 55;
protected $labelGapMm = 2;
protected $url = 'https://open.xpyun.net/api/openapi/xprinter/';
public function __construct()
{
$config = function_exists('config') ? config('printer') : [];
if (!is_array($config) || empty($config)) {
$configFile = __DIR__ . '/../../extra/printer.php';
$config = file_exists($configFile) ? include $configFile : [];
}
$config = is_array($config) ? $config : [];
// 后台配置优先;空字段保留文件配置,方便首次迁移时平滑过渡。
try {
$backendConfig = \addons\shopro\model\Config::load('printer');
if (is_array($backendConfig)) {
foreach ($backendConfig as $key => $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 = '<PAGE l="' . $this->formatMillimeters($this->labelGapMm) . '">';
$content .= '<SIZE>' . $this->formatMillimeters($this->labelWidthMm) . ',' . $this->formatMillimeters($this->labelHeightMm) . '</SIZE>';
foreach ($rows as $index => $row) {
$content .= '<TEXT x="8" y="' . (8 + $index * 28) . '" font="9" w="1" h="1" r="0">' . $row . '</TEXT>';
}
return $content . '</PAGE>';
}
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(['<', '>'], ['&lt;', '&gt;'], $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'), '.');
}
}