@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\job;
|
||||
|
||||
use addons\shopro\model\Order;
|
||||
use app\common\library\PrinterService;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
|
||||
class OrderPrint extends BaseJob
|
||||
{
|
||||
public function handle(Job $job, $data)
|
||||
{
|
||||
$orderId = isset($data['order_id']) ? (int)$data['order_id'] : 0;
|
||||
$task = $orderId ? Db::name('shopro_order_print_task')->where('order_id', $orderId)->find() : null;
|
||||
|
||||
if (!$task || in_array((int)$task['status'], [1, 3], true)) {
|
||||
$job->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$order = Order::with('item')->where('id', $orderId)->find();
|
||||
if (!$order) {
|
||||
throw new \RuntimeException('订单不存在');
|
||||
}
|
||||
|
||||
Db::name('shopro_order_print_task')->where('id', $task['id'])->update([
|
||||
'status' => 2,
|
||||
'attempts' => (int)$task['attempts'] + 1,
|
||||
'updated_at' => time(),
|
||||
'last_error' => '',
|
||||
]);
|
||||
|
||||
$manualPrintKey = isset($task['manual_print_key']) ? $task['manual_print_key'] : '';
|
||||
$result = (new PrinterService())->printLabelOrder($this->buildOrderData($order, $manualPrintKey));
|
||||
if (empty($result['accepted'])) {
|
||||
$this->failTask($task['id'], !empty($result['error']) ? $result['error'] : '云打印未受理', $result);
|
||||
$job->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
Db::name('shopro_order_print_task')->where('id', $task['id'])->update([
|
||||
// 云端已接单,仍需查询 queryOrderState 确认物理打印完成。
|
||||
'status' => 3,
|
||||
'cloud_order_id' => $result['cloud_order_id'],
|
||||
'printer_response' => $result['response'],
|
||||
'updated_at' => time(),
|
||||
'last_error' => '',
|
||||
]);
|
||||
\think\Queue::later(30, '\\addons\\shopro\\job\\OrderPrintStatus@handle', ['task_id' => $task['id']], 'shopro-high');
|
||||
$job->delete();
|
||||
} catch (\Exception $e) {
|
||||
if ($task) {
|
||||
$this->failTask($task['id'], $e->getMessage(), []);
|
||||
}
|
||||
\think\Log::error('订单标签打印失败,订单ID ' . $orderId . ': ' . $e->getMessage());
|
||||
$job->delete();
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildOrderData($order, $manualPrintKey = '')
|
||||
{
|
||||
$items = [];
|
||||
foreach ((array)$order['item'] as $item) {
|
||||
$items[] = [
|
||||
'name' => (isset($item['goods_title']) ? $item['goods_title'] : '商品') . (!empty($item['goods_sku_text']) ? ' (' . $item['goods_sku_text'] . ')' : ''),
|
||||
'num' => isset($item['goods_num']) ? $item['goods_num'] : 1,
|
||||
'price' => isset($item['goods_price']) ? $item['goods_price'] : '0.00',
|
||||
];
|
||||
}
|
||||
|
||||
$address = (isset($order['province_name']) ? $order['province_name'] : '')
|
||||
. (isset($order['city_name']) ? $order['city_name'] : '')
|
||||
. (isset($order['area_name']) ? $order['area_name'] : '')
|
||||
. (isset($order['address']) ? $order['address'] : '');
|
||||
|
||||
return [
|
||||
'order_sn' => $order['order_sn'],
|
||||
'createtime' => $order['createtime'],
|
||||
'consignee' => isset($order['consignee']) ? $order['consignee'] : '客户',
|
||||
'phone' => isset($order['phone']) ? $order['phone'] : '',
|
||||
'address' => $address ?: '到店自提/商家配送',
|
||||
'total_fee' => isset($order['pay_fee']) ? sprintf('%.2f', $order['pay_fee']) : $order['total_fee'],
|
||||
'items' => $items,
|
||||
'print_idempotent' => $manualPrintKey ?: 'shopro-' . $order['id'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function failTask($taskId, $error, $result)
|
||||
{
|
||||
Db::name('shopro_order_print_task')->where('id', $taskId)->update([
|
||||
'status' => -1,
|
||||
'printer_response' => isset($result['response']) ? $result['response'] : '',
|
||||
'last_error' => mb_substr($error, 0, 500, 'UTF-8'),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\shopro;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\Email;
|
||||
use app\common\library\PrinterService;
|
||||
use app\admin\model\shopro\Config as ConfigModel;
|
||||
use think\Exception;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Shopro配置
|
||||
*
|
||||
* @icon fa fa-cogs
|
||||
* @remark 可以在此增改商城的变量和分组,也可以自定义分组和变量,如果需要删除请从数据库中删除
|
||||
*/
|
||||
class Config extends Backend
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \app\admin\model\shopro\Config
|
||||
*/
|
||||
protected $model = null;
|
||||
protected $noNeedRight = ['check', 'rulelist'];
|
||||
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
$this->model = model('app\admin\model\shopro\Config');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$siteList = [];
|
||||
$groupList = ConfigModel::getGroupList();
|
||||
foreach ($groupList as $k => $v) {
|
||||
$siteList[$k]['name'] = $k;
|
||||
$siteList[$k]['title'] = $v;
|
||||
$siteList[$k]['list'] = [];
|
||||
}
|
||||
|
||||
foreach ($this->model->all() as $k => $v) {
|
||||
if (!isset($siteList[$v['group']])) {
|
||||
continue;
|
||||
}
|
||||
$value = $v->toArray();
|
||||
$value['title'] = __($value['title']);
|
||||
if (in_array($value['type'], ['select', 'selects', 'checkbox', 'radio'])) {
|
||||
$value['value'] = explode(',', $value['value']);
|
||||
}
|
||||
$value['content'] = json_decode($value['content'], true);
|
||||
$value['tip'] = htmlspecialchars($value['tip']);
|
||||
$siteList[$v['group']]['list'][] = $value;
|
||||
}
|
||||
$index = 0;
|
||||
foreach ($siteList as $k => &$v) {
|
||||
$v['active'] = !$index ? true : false;
|
||||
$index++;
|
||||
}
|
||||
$this->view->assign('siteList', $siteList);
|
||||
$this->view->assign('typeList', ConfigModel::getTypeList());
|
||||
$this->view->assign('ruleList', ConfigModel::getRegexList());
|
||||
$this->view->assign('groupList', ConfigModel::getGroupList());
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
public function add()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$this->token();
|
||||
$params = $this->request->post("row/a", [], 'trim');
|
||||
if ($params) {
|
||||
foreach ($params as $k => &$v) {
|
||||
$v = is_array($v) ? implode(',', $v) : $v;
|
||||
}
|
||||
try {
|
||||
if (in_array($params['type'], ['select', 'selects', 'checkbox', 'radio', 'array'])) {
|
||||
$params['content'] = json_encode(ConfigModel::decode($params['content']), JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$params['content'] = '';
|
||||
}
|
||||
$result = $this->model->create($params);
|
||||
if ($result !== false) {
|
||||
$this->success();
|
||||
} else {
|
||||
$this->error($this->model->getError());
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->error(__('Parameter %s can not be empty', ''));
|
||||
}
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
* @param null $ids
|
||||
*/
|
||||
public function edit($ids = null)
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$this->token();
|
||||
$row = $this->request->post("row/a", [], 'trim');
|
||||
if ($row) {
|
||||
$configList = [];
|
||||
foreach ($this->model->all() as $v) {
|
||||
if (isset($row[$v['name']])) {
|
||||
$value = $row[$v['name']];
|
||||
if (is_array($value) && isset($value['field'])) {
|
||||
$value = json_encode(ConfigModel::getArrayData($value), JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$value = is_array($value) ? implode(',', $value) : $value;
|
||||
}
|
||||
$v['value'] = $value;
|
||||
$configList[] = $v->toArray();
|
||||
}
|
||||
}
|
||||
$this->model->allowField(true)->saveAll($configList);
|
||||
try {
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
$this->success();
|
||||
}
|
||||
$this->error(__('Parameter %s can not be empty', ''));
|
||||
}
|
||||
}
|
||||
|
||||
public function platform($type)
|
||||
{
|
||||
$action = $this->request->request('action', '');
|
||||
if ($type === 'printer' && $action === 'status') {
|
||||
return $this->printerStatus();
|
||||
}
|
||||
if ($type === 'printer' && $action === 'test') {
|
||||
if (!$this->request->isPost()) {
|
||||
$this->error('测试打印仅支持 POST 请求');
|
||||
}
|
||||
return $this->printerTest();
|
||||
}
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
$data = $this->request->post("data");
|
||||
if ($data) {
|
||||
try {
|
||||
$config = $this->model->get(['name' => $type]);
|
||||
if(!$config) {
|
||||
$this->model->allowField(true)->save([
|
||||
'name' => $type,
|
||||
'title' => $this->request->post("title"),
|
||||
'group' => $this->request->post("group"),
|
||||
'type' => 'array',
|
||||
'value' => $data,
|
||||
]);
|
||||
}else {
|
||||
$config->value = $data;
|
||||
$config->save();
|
||||
}
|
||||
|
||||
if ($type == 'chat') {
|
||||
// 存为文件
|
||||
file_put_contents(
|
||||
ROOT_PATH . 'addons' . DS . 'shopro' . DS . 'library' . DS . 'chat' . DS . 'config.php',
|
||||
'<?php' . "\n\nreturn " . var_export(json_decode($data, true), true) . ";"
|
||||
);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
$this->success();
|
||||
}
|
||||
$this->error(__('Parameter %s can not be empty', ''));
|
||||
}
|
||||
$config = $this->model->where(['name' => $type])->value('value');
|
||||
$config = json_decode($config, true);
|
||||
if ($type === 'wxOfficialAccount') {
|
||||
//动态解析微信公众号服务端Api Url地址域名
|
||||
$config['url'] = request()->domain() . '/addons/shopro/wechat';
|
||||
|
||||
}
|
||||
if($type === 'user') {
|
||||
$this->assignconfig('groupList', \app\admin\model\UserGroup::field('id,name,status')->select());
|
||||
}
|
||||
$this->assignconfig('row', $config);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
protected function printerStatus()
|
||||
{
|
||||
$result = (new PrinterService())->queryStatus();
|
||||
$online = !empty($result['accepted']) && (int)$result['data'] === 1;
|
||||
$message = $online ? '打印机在线' : (!empty($result['error']) ? $result['error'] : '打印机离线');
|
||||
|
||||
$this->success($message, '', [
|
||||
'online' => $online,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function printerTest()
|
||||
{
|
||||
$timestamp = time();
|
||||
$result = (new PrinterService())->printLabelOrder([
|
||||
'order_sn' => 'TEST-' . date('YmdHis', $timestamp),
|
||||
'createtime' => $timestamp,
|
||||
'consignee' => '系统测试',
|
||||
'phone' => '',
|
||||
'address' => '后台标签打印测试,请忽略',
|
||||
'total_fee' => '0.00',
|
||||
'items' => [[
|
||||
'name' => '测试标签(请忽略)',
|
||||
'num' => 1,
|
||||
'price' => '0.00',
|
||||
]],
|
||||
'print_idempotent' => 'shopro-admin-test-' . $timestamp . '-' . mt_rand(1000, 9999),
|
||||
], 1);
|
||||
|
||||
if (empty($result['accepted'])) {
|
||||
$this->error(!empty($result['error']) ? $result['error'] : '测试标签未被芯烨云受理');
|
||||
}
|
||||
|
||||
$printed = false;
|
||||
$cloudOrderId = $result['cloud_order_id'];
|
||||
for ($attempt = 0; $attempt < 3; $attempt++) {
|
||||
$state = (new PrinterService())->queryOrderState($cloudOrderId);
|
||||
if (!empty($state['printed'])) {
|
||||
$printed = true;
|
||||
break;
|
||||
}
|
||||
usleep(500000);
|
||||
}
|
||||
|
||||
$message = $printed ? '测试标签已打印' : '测试标签已送达芯烨云,等待打印机完成';
|
||||
$this->success($message, '', [
|
||||
'printed' => $printed,
|
||||
'cloud_order_id' => $cloudOrderId,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param string $ids
|
||||
*/
|
||||
public function del($ids = "")
|
||||
{
|
||||
$name = $this->request->post('name');
|
||||
$config = ConfigModel::getByName($name);
|
||||
if ($name && $config) {
|
||||
try {
|
||||
$config->delete();
|
||||
$this->refreshFile();
|
||||
} catch (Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
$this->success();
|
||||
} else {
|
||||
$this->error(__('Invalid parameters'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新配置文件
|
||||
*/
|
||||
protected function refreshFile()
|
||||
{
|
||||
$config = [];
|
||||
foreach ($this->model->all() as $k => $v) {
|
||||
$value = $v->toArray();
|
||||
if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
|
||||
$value['value'] = explode(',', $value['value']);
|
||||
}
|
||||
if ($value['type'] == 'array') {
|
||||
$value['value'] = (array)json_decode($value['value'], true);
|
||||
}
|
||||
$config[$value['name']] = $value['value'];
|
||||
}
|
||||
file_put_contents(
|
||||
APP_PATH . 'extra' . DS . 'site.php',
|
||||
'<?php' . "\n\nreturn " . var_export($config, true) . ";"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测配置项是否存在
|
||||
* @internal
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$params = $this->request->post("row/a");
|
||||
if ($params) {
|
||||
$config = $this->model->get($params);
|
||||
if (!$config) {
|
||||
return $this->success();
|
||||
} else {
|
||||
return $this->error(__('Name already exist'));
|
||||
}
|
||||
} else {
|
||||
return $this->error(__('Invalid parameters'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则列表
|
||||
* @internal
|
||||
*/
|
||||
public function rulelist()
|
||||
{
|
||||
//主键
|
||||
$primarykey = $this->request->request("keyField");
|
||||
//主键值
|
||||
$keyValue = $this->request->request("keyValue", "");
|
||||
|
||||
$keyValueArr = array_filter(explode(',', $keyValue));
|
||||
$regexList = \app\common\model\Config::getRegexList();
|
||||
$list = [];
|
||||
foreach ($regexList as $k => $v) {
|
||||
if ($keyValueArr) {
|
||||
if (in_array($k, $keyValueArr)) {
|
||||
$list[] = ['id' => $k, 'name' => $v];
|
||||
}
|
||||
} else {
|
||||
$list[] = ['id' => $k, 'name' => $v];
|
||||
}
|
||||
}
|
||||
return json(['list' => $list]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
* @internal
|
||||
*/
|
||||
public function emailtest()
|
||||
{
|
||||
$row = $this->request->post('row/a');
|
||||
$receiver = $this->request->post("receiver");
|
||||
if ($receiver) {
|
||||
if (!Validate::is($receiver, "email")) {
|
||||
$this->error(__('Please input correct email'));
|
||||
}
|
||||
\think\Config::set('site', array_merge(\think\Config::get('site'), $row));
|
||||
$email = new Email;
|
||||
$result = $email
|
||||
->to($receiver)
|
||||
->subject(__("This is a test mail"))
|
||||
->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . __('This is a test mail content') . '</div>')
|
||||
->send();
|
||||
if ($result) {
|
||||
$this->success();
|
||||
} else {
|
||||
$this->error($email->getError());
|
||||
}
|
||||
} else {
|
||||
return $this->error(__('Invalid parameters'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,298 @@
|
||||
<?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('后台未启用标签打印');
|
||||
}
|
||||
|
||||
$content = $this->buildLabelContent($orderData);
|
||||
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 = [])
|
||||
{
|
||||
$title = $this->labelText(isset($order['shop_name']) ? $order['shop_name'] : $this->shopName, 12);
|
||||
$orderSn = $this->barcodeText(isset($order['order_sn']) ? $order['order_sn'] : 'TEST' . date('YmdHis'));
|
||||
$itemSummary = $this->buildItemSummary(isset($order['items']) && is_array($order['items']) ? $order['items'] : []);
|
||||
$totalAmount = $this->labelText(isset($order['total_fee']) ? $order['total_fee'] : '0.00', 12);
|
||||
$consignee = $this->labelText(isset($order['consignee']) ? $order['consignee'] : '客户', 10);
|
||||
$phone = $this->labelText(isset($order['phone']) ? $order['phone'] : '', 16);
|
||||
$addressLines = $this->splitLabelLines(isset($order['address']) ? $order['address'] : '到店自提/商家配送', 15, 3);
|
||||
|
||||
$heightDots = (int)round($this->labelHeightMm * 8);
|
||||
$margin = 8;
|
||||
$barcodeY = 56;
|
||||
$barcodeHeight = 64;
|
||||
$itemY = $barcodeY + $barcodeHeight + 12;
|
||||
$quantityY = $itemY + 28;
|
||||
$totalY = $quantityY + 28;
|
||||
|
||||
// 40 x 30mm is the official example size. Keep the final text inside smaller custom labels.
|
||||
if ($totalY + 24 > $heightDots) {
|
||||
$barcodeY = 48;
|
||||
$barcodeHeight = max(40, min(56, $heightDots - 136));
|
||||
$itemY = $barcodeY + $barcodeHeight + 8;
|
||||
$quantityY = $itemY + 24;
|
||||
$totalY = $quantityY + 24;
|
||||
}
|
||||
|
||||
$content = '<PAGE l="' . $this->formatMillimeters($this->labelGapMm) . '">';
|
||||
$content .= '<SIZE>' . $this->formatMillimeters($this->labelWidthMm) . ',' . $this->formatMillimeters($this->labelHeightMm) . '</SIZE>';
|
||||
$content .= '<TEXT x="' . $margin . '" y="8" font="9" w="1" h="1" r="0">' . $title . '</TEXT>';
|
||||
$content .= '<TEXT x="' . $margin . '" y="32" font="9" w="1" h="1" r="0">订单号:' . $this->labelText($orderSn, 18) . '</TEXT>';
|
||||
$content .= '<BC128 x="' . $margin . '" y="' . $barcodeY . '" h="' . $barcodeHeight . '" s="0" n="1" w="1" r="0">' . $orderSn . '</BC128>';
|
||||
$content .= '<TEXT x="' . $margin . '" y="' . $itemY . '" font="9" w="1" h="1" r="0">商品:' . $itemSummary['name'] . '</TEXT>';
|
||||
$content .= '<TEXT x="' . $margin . '" y="' . $quantityY . '" font="9" w="1" h="1" r="0">数量:' . $itemSummary['quantity'] . '</TEXT>';
|
||||
$content .= '<TEXT x="' . $margin . '" y="' . $totalY . '" font="9" w="1" h="1" r="0">金额:' . $totalAmount . '</TEXT>';
|
||||
|
||||
// 55 x 55mm labels leave enough room for the delivery information below the order summary.
|
||||
if ($heightDots >= 400) {
|
||||
$receiverY = $totalY + 32;
|
||||
$phoneY = $receiverY + 28;
|
||||
$addressY = $phoneY + 28;
|
||||
$content .= '<TEXT x="' . $margin . '" y="' . $receiverY . '" font="9" w="1" h="1" r="0">收货人:' . $consignee . '</TEXT>';
|
||||
$content .= '<TEXT x="' . $margin . '" y="' . $phoneY . '" font="9" w="1" h="1" r="0">手机号:' . $phone . '</TEXT>';
|
||||
foreach ($addressLines as $index => $addressLine) {
|
||||
$prefix = $index === 0 ? '地址:' : '';
|
||||
$content .= '<TEXT x="' . $margin . '" y="' . ($addressY + $index * 28) . '" font="9" w="1" h="1" r="0">' . $prefix . $this->labelText($addressLine, 15) . '</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(['<', '>'], ['<', '>'], $text);
|
||||
}
|
||||
|
||||
protected function barcodeText($value)
|
||||
{
|
||||
$text = preg_replace('/[^\x20-\x7E]/', '', $this->cleanText($value));
|
||||
return $text !== '' ? substr($text, 0, 50) : 'TEST' . date('YmdHis');
|
||||
}
|
||||
|
||||
protected function buildItemSummary(array $items)
|
||||
{
|
||||
$firstName = '商品';
|
||||
$quantity = 0;
|
||||
foreach ($items as $index => $item) {
|
||||
if ($index === 0 && isset($item['name'])) {
|
||||
$firstName = $item['name'];
|
||||
}
|
||||
$quantity += isset($item['num']) ? max(1, (int)$item['num']) : 1;
|
||||
}
|
||||
if (count($items) > 1) {
|
||||
$firstName .= '等' . count($items) . '件';
|
||||
}
|
||||
return [
|
||||
'name' => $this->labelText($firstName, 12),
|
||||
'quantity' => $quantity ?: 1,
|
||||
];
|
||||
}
|
||||
|
||||
protected function splitLabelLines($value, $lineWidth, $maxLines)
|
||||
{
|
||||
$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;
|
||||
if (count($lines) >= $maxLines) {
|
||||
return $lines;
|
||||
}
|
||||
$line = '';
|
||||
$lineWidthUsed = 0;
|
||||
}
|
||||
$line .= $character;
|
||||
$lineWidthUsed += $characterWidth;
|
||||
}
|
||||
|
||||
if ($line !== '' || empty($lines)) {
|
||||
$lines[] = $line ?: '到店自提/商家配送';
|
||||
}
|
||||
return array_slice($lines, 0, $maxLines);
|
||||
}
|
||||
|
||||
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'), '.');
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户