cloudPrintUtil.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. namespace common\components;
  3. use Yii;
  4. /**
  5. * 链科云打印 v3 业务封装,配置读取 .env
  6. */
  7. class cloudPrintUtil
  8. {
  9. /** @var CloudPrinter|null */
  10. protected static $client;
  11. /** @var array|null */
  12. protected static $printerConfig;
  13. /**
  14. * 是否启用链科云打印(.env LIANKENET_ENABLED=1 且密钥齐全)
  15. */
  16. public static function isEnabled()
  17. {
  18. if (getenv('LIANKENET_ENABLED') != '1') {
  19. return false;
  20. }
  21. return !empty(getenv('LIANKENET_API_KEY'))
  22. && !empty(getenv('LIANKENET_DEVICE_ID'))
  23. && !empty(getenv('LIANKENET_DEVICE_KEY'));
  24. }
  25. /**
  26. * 从 .env 创建链科客户端
  27. */
  28. public static function createClient()
  29. {
  30. if (!self::isEnabled()) {
  31. throw new CloudPrinterApiException('链科云打印未启用或配置不完整');
  32. }
  33. if (self::$client instanceof CloudPrinter) {
  34. return self::$client;
  35. }
  36. self::$client = new CloudPrinter(
  37. getenv('LIANKENET_API_KEY'),
  38. getenv('LIANKENET_DEVICE_ID'),
  39. getenv('LIANKENET_DEVICE_KEY'),
  40. getenv('LIANKENET_DEBUG') == '1',
  41. intval(getenv('LIANKENET_TIMEOUT') ?: 10),
  42. getenv('LIANKENET_SERVER') ?: 'https://cloud.liankenet.com/'
  43. );
  44. return self::$client;
  45. }
  46. /**
  47. * 打印订单小票(与 printUtil::printMsg 入参一致,便于切换)
  48. */
  49. public function printMsg($content, $shop = null, $orderSn = '')
  50. {
  51. $plainText = $this->feieContentToText($content);
  52. if ($plainText === '') {
  53. return false;
  54. }
  55. $chunks = $this->splitContent($plainText, 4500);
  56. foreach ($chunks as $chunk) {
  57. $this->printTextChunk($chunk, $shop, $orderSn);
  58. }
  59. return true;
  60. }
  61. /**
  62. * 打印标签内容(链科走文本任务,飞鹅标签指令无法直接复用)
  63. */
  64. public function printLabelMsg($content)
  65. {
  66. $plainText = $this->feieContentToText($content);
  67. if ($plainText === '') {
  68. return false;
  69. }
  70. return $this->printTextChunk($plainText);
  71. }
  72. /**
  73. * 直接打印本地文件
  74. */
  75. public function printFile($filePath, $optionalArray = [], $paperSize = null)
  76. {
  77. $client = self::createClient();
  78. $printer = $this->resolvePrinterConfig($client);
  79. $paperSize = $paperSize ?? intval(getenv('LIANKENET_PAPER_SIZE') ?: 9);
  80. return $client->addJobByPath(
  81. $printer['port'],
  82. $printer['model'],
  83. $paperSize,
  84. $filePath,
  85. $optionalArray
  86. );
  87. }
  88. /**
  89. * 打印 HTML 页面(A4 配送单等排版页面)
  90. */
  91. public function printHtml($html, $paperSize = null, $optionalArray = [])
  92. {
  93. $html = trim((string)$html);
  94. if ($html === '') {
  95. return false;
  96. }
  97. $tempFile = $this->createTempHtmlFile($html);
  98. try {
  99. return $this->printFile($tempFile, $optionalArray, $paperSize);
  100. } finally {
  101. if (is_file($tempFile)) {
  102. @unlink($tempFile);
  103. }
  104. }
  105. }
  106. /**
  107. * 生成可打印 HTML 临时文件
  108. */
  109. protected function createTempHtmlFile($html)
  110. {
  111. $tempFile = tempnam(sys_get_temp_dir(), 'lk_print_');
  112. if ($tempFile === false) {
  113. throw new CloudPrinterApiException('无法创建临时打印文件');
  114. }
  115. $target = $tempFile . '.html';
  116. @unlink($tempFile);
  117. file_put_contents($target, $html);
  118. return $target;
  119. }
  120. /**
  121. * 飞鹅排版标签转纯文本
  122. */
  123. protected function feieContentToText($content)
  124. {
  125. $text = str_ireplace(['<BR>', '<br>', '<br/>', '<br />'], "\n", (string)$content);
  126. $text = preg_replace('/<\/?CB>/i', '', $text);
  127. $text = preg_replace('/<\/?B>/i', '', $text);
  128. $text = preg_replace('/<\/?C>/i', '', $text);
  129. $text = preg_replace('/<QR>.*?<\/QR>/is', '', $text);
  130. $text = preg_replace('/<[^>]+>/', '', $text);
  131. $text = str_replace(["\r\n", "\r"], "\n", $text);
  132. return trim($text);
  133. }
  134. /**
  135. * 长内容分段,避免单文件过大
  136. */
  137. protected function splitContent($text, $maxLength = 4500)
  138. {
  139. if (strlen($text) <= $maxLength) {
  140. return [$text];
  141. }
  142. $lines = explode("\n", $text);
  143. $chunks = [];
  144. $current = '';
  145. foreach ($lines as $line) {
  146. $candidate = $current === '' ? $line : $current . "\n" . $line;
  147. if (strlen($candidate) > $maxLength) {
  148. if ($current !== '') {
  149. $chunks[] = $current;
  150. }
  151. $current = $line;
  152. } else {
  153. $current = $candidate;
  154. }
  155. }
  156. if ($current !== '') {
  157. $chunks[] = $current;
  158. }
  159. return $chunks;
  160. }
  161. /**
  162. * 提交一段文本为打印任务
  163. */
  164. protected function printTextChunk($text, $shop = null, $orderSn = '')
  165. {
  166. $tempFile = $this->createTempTextFile($text);
  167. try {
  168. $client = self::createClient();
  169. $printer = $this->resolvePrinterConfig($client);
  170. $paperSize = intval(getenv('LIANKENET_PAPER_SIZE') ?: 9);
  171. $client->addJobByPath(
  172. $printer['port'],
  173. $printer['model'],
  174. $paperSize,
  175. $tempFile,
  176. $this->buildOptionalParams()
  177. );
  178. } catch (\Throwable $e) {
  179. Yii::error('链科云打印失败: ' . $e->getMessage(), __METHOD__);
  180. $shopName = '';
  181. if (is_object($shop)) {
  182. $shopName = ($shop->merchantName ?? '') . ($shop->shopName ?? '');
  183. }
  184. noticeUtil::push(
  185. trim($shopName . ' 链科云打印未成功,订单号:' . $orderSn . ',原因:' . $e->getMessage()),
  186. '15280215347'
  187. );
  188. return false;
  189. } finally {
  190. if (is_file($tempFile)) {
  191. @unlink($tempFile);
  192. }
  193. }
  194. return true;
  195. }
  196. /**
  197. * 生成 UTF-8 BOM 文本临时文件,便于 Windows 打印盒识别中文
  198. */
  199. protected function createTempTextFile($text)
  200. {
  201. $tempFile = tempnam(sys_get_temp_dir(), 'lk_print_');
  202. if ($tempFile === false) {
  203. throw new CloudPrinterApiException('无法创建临时打印文件');
  204. }
  205. $target = $tempFile . '.txt';
  206. @unlink($tempFile);
  207. file_put_contents($target, "\xEF\xBB\xBF" . $text);
  208. return $target;
  209. }
  210. /**
  211. * 解析打印机 USB 口与驱动名:优先 .env,否则取列表首台
  212. */
  213. protected function resolvePrinterConfig(CloudPrinter $client)
  214. {
  215. if (is_array(self::$printerConfig)) {
  216. return self::$printerConfig;
  217. }
  218. $port = trim((string)getenv('LIANKENET_DEVICE_PORT'));
  219. $model = trim((string)getenv('LIANKENET_PRINTER_MODEL'));
  220. if ($port !== '' && $model !== '') {
  221. self::$printerConfig = ['port' => $port, 'model' => $model];
  222. return self::$printerConfig;
  223. }
  224. $list = $client->getPrinterList();
  225. if (empty($list)) {
  226. throw new CloudPrinterApiException('链科设备下没有可用打印机');
  227. }
  228. $printer = $list[0];
  229. self::$printerConfig = [
  230. 'port' => $printer->port ?? '',
  231. 'model' => $printer->driver_name ?? '',
  232. ];
  233. if (self::$printerConfig['port'] === '' || self::$printerConfig['model'] === '') {
  234. throw new CloudPrinterApiException('链科打印机信息不完整');
  235. }
  236. return self::$printerConfig;
  237. }
  238. /**
  239. * 可选打印参数(份数、颜色、方向等,按 v3 文档扩展)
  240. */
  241. protected function buildOptionalParams()
  242. {
  243. $params = [];
  244. $copies = intval(getenv('LIANKENET_COPIES') ?: 1);
  245. if ($copies > 1) {
  246. $params['dmCopies'] = $copies;
  247. }
  248. return $params;
  249. }
  250. }