Initial commit: 小众厨房项目代码

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
可乐加冰
2026-09-23 17:04:10 +08:00
当前提交 f7d3d7d1bd
共有 1761 个文件被更改,包括 453668 次插入 和 0 次删除

查看文件

@@ -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(['<', '>'], ['&lt;', '&gt;'], $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'), '.');
}
}