[СДЕЛАЙ САМ] Генерация, вставка в PDF и последующее чтение QR-кодов на сайте
Всем привет! Всё как всегда, сделал сам, делюсь с другими. Конструктивная критика приветствуется.
Задача: организовать продажу билетов с онлайн оплатой на массовые мероприятия, организуемые заказчиком. Выбор мест не требуется, ограничения только по количеству билетов. Организовать отправку купленных билетов на почту покупателя в формате pdf. Создать систему проверки билетов по qr-коду. Дать возможность администратору сайта оформлять произвольное количество билетов для продажи на входе. Создавать резерв билетов. Закрывать продажу, при отсутствии билетов.
Нам понадобится:
Информацию о том как работать с MPDF я брал из официальной документации, она у них, насколько я могу судить, нормальная. О том как работать с PHP QR Code прочитал тут.
С предисловием вроде всё. Переходим к решению.
Первое, что нужно изменить в стандартной логике minishop2, это процесс формирования заказа, поскольку нам не нужна корзина и Билет, хотя и один товара, но он должен иметь уникальный номер и возможность быть активированным отдельно от всего заказа. К тому же заказчик просил записывать дату и время активации, сотрудника который активировал и мало ли что ещё ему(заказчику) придёт в голову потом. Поэтому я принял решение переопределить метод submit класса core/components/minishop2/model/minishop2/msorderhandler.class.php, но при это постарался сохранить стандартную логику, на всякий случай. В местах изменений я оставил комментарии.
Поскольку нам нужно отправить письмо с вложением, а стандартный метод minishop2 так не умеет, отправку писем менеджеру и клиенту в настройках minishop2 отключаем. И создаем сниппет sendTickets.php.
Важный момент: чтобы в письмо добавить ссылку на оплату (мало ли что) мы подключаем класс minishop2.
Чтобы избежать повторных отправок, записываем флаг отправки в поле properties заказа, при этом оставляем возможность повторной отправки билетов администраторам, также для этого при расширении класса мы записали ссылку на страницу domain.ru?msorder=1 в поле comment. Чтобы отобразить его в админке, нужно прописать его в системной настройке ms2_order_grid_fields и отредактировать файл assets/components/minishop2/js/mgr/orders/orders.grid.js, а именно на 40 строке, примерно добавить такой код
Запускать его мы будем на странице вида domain.ru?msorder=1 после загрузки страницы или на странице успешной оплаты с помощью ajax.
На странице нужно разместить вот такой код
Для того, чтобы запрос принять и обработать создаем в папке assets или любом другом доступном с фронта месте файл ajaxReceiver.php
Круг замкнулся и теперь нужно сгенерировать билет. За это отвечает сниппет generateTickets.php
Ну и наконец непосредственно отправка в сниппите sendEmail.php
На этом формирование и отправка билетов закончены, но нужно ещё написать плагин на событие msOnSubmitOrder, который будет проверять есть ли запрошенное пользователем количество билетов или нет. При этом админу доступен резерв.
Осталось создать страницу для считывания qr-кодов и их проверки. HTML такой
БОНУС сниппет для проверки доступности покупки билетов на фронте, надо же сообщить что билеты проданы.
Задача: организовать продажу билетов с онлайн оплатой на массовые мероприятия, организуемые заказчиком. Выбор мест не требуется, ограничения только по количеству билетов. Организовать отправку купленных билетов на почту покупателя в формате pdf. Создать систему проверки билетов по qr-коду. Дать возможность администратору сайта оформлять произвольное количество билетов для продажи на входе. Создавать резерв билетов. Закрывать продажу, при отсутствии билетов.
Нам понадобится:
- minishop2;
- библиотека jsqr.js для считывания qr-кодов;
- библиотека mpdf для генерации pdf;
- библиотека PHP QR Code для генерации qr-кодов;
Информацию о том как работать с MPDF я брал из официальной документации, она у них, насколько я могу судить, нормальная. О том как работать с PHP QR Code прочитал тут.
С предисловием вроде всё. Переходим к решению.
Первое, что нужно изменить в стандартной логике minishop2, это процесс формирования заказа, поскольку нам не нужна корзина и Билет, хотя и один товара, но он должен иметь уникальный номер и возможность быть активированным отдельно от всего заказа. К тому же заказчик просил записывать дату и время активации, сотрудника который активировал и мало ли что ещё ему(заказчику) придёт в голову потом. Поэтому я принял решение переопределить метод submit класса core/components/minishop2/model/minishop2/msorderhandler.class.php, но при это постарался сохранить стандартную логику, на всякий случай. В местах изменений я оставил комментарии.
<?php
class customOrderHandler extends msOrderHandler
{
/**
* @param array $data
*
* @return array|string
*/
public function submit($data = array())
{
$response = $this->ms2->invokeEvent('msOnSubmitOrder', array(
'data' => $data,
'order' => $this,
));
if (!$response['success']) {
return $this->error($response['message']);
}
if (!empty($response['data']['data'])) {
$this->set($response['data']['data']);
}
$response = $this->getDeliveryRequiresFields();
if ($this->ms2->config['json_response']) {
$response = json_decode($response, true);
}
if (!$response['success']) {
return $this->error($response['message']);
}
$requires = $response['data']['requires'];
$errors = array();
foreach ($requires as $v) {
if (!empty($v) && empty($this->order[$v])) {
$errors[] = $v;
}
}
if (!empty($errors)) {
return $this->error('ms2_order_err_requires', $errors);
}
$user_id = $this->ms2->getCustomerId();
if (empty($user_id) || !is_int($user_id)) {
return $this->error(is_string($user_id) ? $user_id : 'ms2_err_user_nf');
}
$cart_status = $this->ms2->cart->status();
if (empty($cart_status['total_count']) && empty((int)$_POST['total_count'])) {
return $this->error('ms2_order_err_empty');
}
$delivery_cost = $this->getCost(false, true);
$cart_cost = $this->getCost(true, true) - $delivery_cost ?: (int)$_POST['total_count'] * (float)$_POST['price']; // изменена логика расчёта стоимости
$createdon = date('Y-m-d H:i:s');
/** @var msOrder $order */
$order = $this->modx->newObject('msOrder');
$order->fromArray(array(
'user_id' => $user_id,
'createdon' => $createdon,
'num' => $this->getNum(),
'delivery' => $this->order['delivery'],
'payment' => $this->order['payment'],
'cart_cost' => $cart_cost,
'weight' => $cart_status['total_weight'],
'delivery_cost' => $delivery_cost,
'cost' => ($cart_cost + $delivery_cost),
'status' => 0,
'context' => $this->ms2->config['ctx']
));
// Adding address
/** @var msOrderAddress $address */
$address = $this->modx->newObject('msOrderAddress');
$address->fromArray(array_merge($this->order, array(
'user_id' => $user_id,
'createdon' => $createdon,
)));
$order->addOne($address);
// Adding products
$cart = $this->ms2->cart->get();
$products = array();
if($cart){
foreach ($cart as $v) {
if ($tmp = $this->modx->getObject('msProduct', array('id' => $v['id']))) {
$name = $tmp->get('pagetitle');
} else {
$name = '';
}
/** @var msOrderProduct $product */
$product = $this->modx->newObject('msOrderProduct');
$product->fromArray(array_merge($v, array(
'product_id' => $v['id'],
'name' => $name,
'cost' => $v['price'] * $v['count'],
)));
$products[] = $product;
}
}
/* custom code */
else{
$resource = $this->modx->getObject('msProduct', array('id' => (int)$_POST['id']));
$props = $order->get('properties');
$props['pdf_tpl'] = $resource->get('description');
$order->set('properties', $props);
if($resource){
$price = $resource->get('price');
$options = array(
'activated' => 0,
'activatedon' => '',
'activatedby' => '',
'event_date' => $resource->getTVValue('event_date')
);
for($i = 1; $i <= $_POST['total_count']; $i++){
$ticketNum = $order->get('num') . '-'. $i;
$ticketName = $resource->get('pagetitle') .' №' . $ticketNum;
/** @var msOrderProduct $product */
$product = $this->modx->newObject('msOrderProduct');
$product->fromArray(array(
'product_id' => (int)$_POST['id'],
'name' => $ticketName,
'cost' => $price,
'price' => $price,
'count' => 1,
'options' => $options
));
$product->save();
$products[] = $product;
}
}
}
/* custom code */
$order->addMany($products);
$response = $this->ms2->invokeEvent('msOnBeforeCreateOrder', array(
'msOrder' => $order,
'order' => $this,
));
if (!$response['success']) {
return $this->error($response['message']);
}
if ($order->save()) {
$response = $this->ms2->invokeEvent('msOnCreateOrder', array(
'msOrder' => $order,
'order' => $this,
));
if (!$response['success']) {
return $this->error($response['message']);
}
/* custom code */
$order->set('comment', $this->modx->getOption('site_url').'?msorder='.$order->get('id'));
$order->save();
/* custom code */
$this->ms2->cart->clean();
$this->clean();
if (empty($_SESSION['minishop2']['orders'])) {
$_SESSION['minishop2']['orders'] = array();
}
$_SESSION['minishop2']['orders'][] = $order->get('id');
// Trying to set status "new"
/* custom code */
$status = 1;
if((int)$_POST['user_id'] > 0){
$status = 2; // ставим статус Оплачен если заказывает менеджер
}
/* custom code */
$response = $this->ms2->changeOrderStatus($order->get('id'), $status);
if ($response !== true) {
return $this->error($response, array('msorder' => $order->get('id')));
}
// Reload order object after changes in changeOrderStatus method
$order = $this->modx->getObject('msOrder', array('id' => $order->get('id')));
/** @var msPayment $payment */
if ($payment = $this->modx->getObject('msPayment',
array('id' => $order->get('payment'), 'active' => 1))
) {
$response = $payment->send($order);
if ($this->config['json_response']) {
@session_write_close();
exit(is_array($response) ? json_encode($response) : $response);
} else {
if (!empty($response['data']['redirect'])) {
$this->modx->sendRedirect($response['data']['redirect']);
} elseif (!empty($response['data']['msorder'])) {
$this->modx->sendRedirect(
$this->modx->context->makeUrl(
$this->modx->resource->id,
array('msorder' => $response['data']['msorder'])
)
);
} else {
$this->modx->sendRedirect($this->modx->context->makeUrl($this->modx->resource->id));
}
return $this->success();
}
} else {
if ($this->ms2->config['json_response']) {
return $this->success('', array('msorder' => $order->get('id')));
} else {
$this->modx->sendRedirect(
$this->modx->context->makeUrl(
$this->modx->resource->id,
array('msorder' => $response['data']['msorder'])
)
);
return $this->success();
}
}
}
return $this->error();
}
}Поскольку нам нужно отправить письмо с вложением, а стандартный метод minishop2 так не умеет, отправку писем менеджеру и клиенту в настройках minishop2 отключаем. И создаем сниппет sendTickets.php.
<?php
$order = $modx->getObject('msOrder', $order_id);
if($order){
$props =$order->get('properties');
if(!$props['sended'] || ($props['sended'] && $user_id > 0)){
$tickets = array();
$products = $order->getMany('Products');
$address = $order->getOne('Address');
$profile = $modx->getObject('modUserProfile', array('internalKey' => $address->get('user_id')));
$i = 0;
foreach($products as $product){
$ticketName = $product->get('name');
$options = $product->get('options');
$ticket = array(
'name' => $ticketName,
'receiver' => $address->get('receiver'),
'email' => $profile->get('email'),
'phone' => $address->get('phone'),
'event_date' => $options['event_date'],
'price' => $product->get('price'),
'id' => $product->get('id'),
'product_id' => $product->get('product_id')
);
$tickets[] = $ticket;
}
$pdoTools = $modx->getService('pdoTools');
$attach = $pdoTools->runSnippet('@FILE snippets/generateTickets.php', array('tickets' => $tickets, 'chunk' => $props['pdf_tpl']));
if(!$attach){
return json_encode(array('success' => false, 'message' => 'Не удалось сгенерировать билеты. Обратитесь к администратору сайта.'));
}
// получаем ссылку на оплату
$params['link'] = '';
if ($payment = $order->getOne('Payment')) {
if ($class = $payment->get('class')) {
require_once MODX_CORE_PATH . '/components/minishop2/model/minishop2/minishop2.class.php';
$modx->initialize('web');
$ms2 = new miniShop2($modx, array());
$ms2->loadCustomClasses('payment');
if (class_exists($class)) {
/** @var msPaymentHandler|PayPal $handler */
$handler = new $class($order);
if (method_exists($handler, 'getPaymentLink')) {
$params['link'] = $handler->getPaymentLink($order);
}
}
}
}
$params['order'] = $order->toArray();
$params['address'] = $address->toArray();
if(!$pdoTools->runSnippet('@FILE snippets/sendEmail.php', array(
'chunk' => 'core/elements/chunks/emails/sendTicketEmail.html',
'params' => $params,
'to' => $profile->get('email'),
'attachment' => $attach
))){
return json_encode(array('success' => false, 'message' => 'Билеты не отправлены. Обратитесь к администратору сайта.'));
};
$props['sended'] = 1;
$order->set('properties', $props);
$order->save();
return json_encode(array('success' => true, 'message' => 'Билеты отправлены. До скорой встречи!'));
}
else{
return json_encode(array('success' => false, 'message' => 'Билеты уже были отправлены. Проверьте папку СПАМ.'));
}
}
else{
return json_encode(array('success' => false, 'message' => 'Заказ не найден. Обратитесь к администратору сайта.'));
}Важный момент: чтобы в письмо добавить ссылку на оплату (мало ли что) мы подключаем класс minishop2.
Чтобы избежать повторных отправок, записываем флаг отправки в поле properties заказа, при этом оставляем возможность повторной отправки билетов администраторам, также для этого при расширении класса мы записали ссылку на страницу domain.ru?msorder=1 в поле comment. Чтобы отобразить его в админке, нужно прописать его в системной настройке ms2_order_grid_fields и отредактировать файл assets/components/minishop2/js/mgr/orders/orders.grid.js, а именно на 40 строке, примерно добавить такой код
comment: {width: 100, renderer: function(val, cell, row) {
return '<a href="'+val+'" target="_blank">Отправить билеты</a>';
}}, Запускать его мы будем на странице вида domain.ru?msorder=1 после загрузки страницы или на странице успешной оплаты с помощью ajax.
//функция отправки ajax
function sendAjax(path, params, callback) {
const request = new XMLHttpRequest();
const url = path || document.location.href;
request.open('POST', url, true);
request.setRequestHeader("X-Requested-With", "XMLHttpRequest");
request.responseType = 'json';
request.addEventListener('readystatechange', function () {
if (request.readyState === 4 && request.status === 200) {
callback(request.response);
}
});
request.send(params);
}
document.addEventListener('DOMContentLoaded', function(){
let orderid = document.getElementById('orderid');
if(orderid){
let params = new FormData(),
base = document.querySelector('base').href,
id = orderid.value,
userid = document.getElementById('userid').value,
path = base + 'assets/project_files/ajaxReceiver.php';
params.append('order_id', id);
params.append('user_id', userid);
params.append('action', 'sendTickets');
sendAjax(path, params, function (response) {
document.getElementById('sendresult').innerText = response.message;
});
}
});На странице нужно разместить вот такой код
{set $msorder = $.get.msorder ?: $.cookie.msorder}
{if $msorder}
<input type="hidden" id="orderid" value="{$msorder}">
<input type="hidden" id="userid" value="{$_modx->user.id}">
<script src="assets/project_files/js/common.min.js"></script>
{/if} Для того, чтобы запрос принять и обработать создаем в папке assets или любом другом доступном с фронта месте файл ajaxReceiver.php
<?php
define('MODX_API_MODE', true);
require_once dirname(dirname(dirname(__FILE__))) . '/index.php';
$modx->getService('error', 'error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_ERROR);
// Откликаться будет ТОЛЬКО на ajax запросы
if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') {return;}
// Сниппет будет обрабатывать не один вид запросов, поэтому работать будем по запрашиваемому действию
// Если в массиве POST нет действия - выход
if (empty($_POST['action'])) {
return;
}
// А если есть - работаем
$res = '';
$action = $_POST['action'];
$params = json_decode($_POST['params'], 1);
switch ($action) {
case 'activateTicket':
if((int)$_POST['user_id'] > 0){
$id = (int)$_POST['id'];
if ($ticket = $modx->getObject('msOrderProduct', $id)) {
$sql = 'SELECT status FROM modxwb_ms2_orders WHERE id = ' . $ticket->get('order_id');
$statement = $modx->query($sql);
$status = $statement->fetchAll(PDO::FETCH_COLUMN);
$options = $ticket->get('options');
$event_date = strtotime($options['event_date']);
$now = time();
$diff = ($event_date - $now) / 3600;
if($diff < 3 && $diff >= 0){
if ($status[0] == 1) {
$res = json_encode(array('success' => false, 'message' => 'Билет неоплачен'));
} else if ($status[0] == 2 && !$options['activated']) {
$options['activated'] = 1;
$options['activatedon'] = date('Y-m-d H:i');
$options['activatedby'] = $_POST['user_id'];
$order = $modx->getObject('msOrder', $ticket->get('order_id'));
$order->set('status', 3);
$order->save();
$res = json_encode(array('success' => true, 'message' => 'Билет активирован'));
} else if (($status[0] == 3 || $status[0] == 2) && $options['activated']) {
$res = json_encode(array('success' => false, 'message' => 'Повторная активация невозможна'));
} else {
$res = json_encode(array('success' => false, 'message' => 'Активация невозможна'));
}
}
else{
$res = json_encode(array('success' => false, 'message' => 'Регистрация возможна не ранее чем за 3 часа до начала.'));
}
}
else{
$res = json_encode(array('success' => false, 'message' => 'Билет не существует'));
}
}
break;
case 'sendTickets':
$pdoTools = $modx->getService('pdoTools');
$res = $pdoTools->runSnippet('@FILE snippets/sendTickets.php', array('order_id' => (int)$_POST['order_id'], 'user_id' => (int)$_POST['user_id']));
break;
// А вот сюда потом добавлять новые методы prodFastView
}
// Если у нас есть, что отдать на запрос - отдаем и прерываем работу парсера MODX
if (!empty($res)) {
die($res);
}Круг замкнулся и теперь нужно сгенерировать билет. За это отвечает сниппет generateTickets.php
<?php
require_once MODX_CORE_PATH . 'elements/phpqrcode/qrlib.php';
require_once MODX_BASE_PATH . '/vendor/autoload.php';
if (count($tickets)) {
$qrcodePath = MODX_BASE_PATH . 'attachments/qrcodes/qrcode.png';
$pdfPath = MODX_BASE_PATH . 'attachments/tickets/tickets.pdf';
$pdoTools = $modx->getService('pdoTools');
//подключаем шрифты
$defaultConfig = (new Mpdf\Config\ConfigVariables())->getDefaults();
$fontDirs = $defaultConfig['fontDir'];
$defaultFontConfig = (new Mpdf\Config\FontVariables())->getDefaults();
$fontData = $defaultFontConfig['fontdata'];
$mpdf = new \Mpdf\Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'orientation' => 'P',
'fontDir' => array_merge($fontDirs, [
MODX_BASE_PATH . '/assets/project_files/fonts/facefont/',
]),
'fontdata' => $fontData + [
'catorze27style1' => [
'R' => 'catorze27style1-semibold.ttf',
]
],
'default_font' => 'catorze27style1'
]);
foreach ($tickets as $k => $v) {
//генерируем qrCode
$text = json_encode($v);
QRcode::png($text, $qrcodePath, 'H', '3px');
$imageSize = getimagesize($qrcodePath);
$imageData = base64_encode(file_get_contents($qrcodePath));
$v['qr'] = "data:{$imageSize['mime']};base64,{$imageData}";
// добавляем шаблон html
$html = $pdoTools->parseChunk($chunk, $v);
// генерируем PDF
$mpdf->AddPage();
$mpdf->Image(MODX_BASE_PATH. 'assets/project_files/img/common/ticket.jpg',0,0,210,297,'jpg','',true,false, true);
$mpdf->WriteHTML($html);
}
$mpdf->Output($pdfPath);
return $pdfPath;
}Тут стоит пояснить, что получить qr-код и pdf без сохранения в файл у меня не получилось, поэтому к каждому заказу я пересоздаю файлы qrcode.png и tickets.pdf. Если кто-то подскажет как этого избежать, буду благодарен. testTicket.html — это обычный чанк с html кодом, удовлетворяющим требованиям документации mpdf. Функция возвращает путь к созданному файлу.Ну и наконец непосредственно отправка в сниппите sendEmail.php
<?php
$http_host = $modx->getOption('http_host');
$site_name = $modx->getOption('site_name');
if(!isset($chunk)){
$modx->log(1, 'Письмо не отправлено. Не передан чанк');
return false;
}
if(!isset($to)){
$modx->log(1, 'Письмо не отправлено. Не передан email получателя');
return false;
}else{
$to = explode (',',$to);
}
if(!isset($subject)){$subject = 'Вы сделали заказ на сайте '.$site_name;}
if(!isset($from)){$from = 'noreply@'.$http_host;}
if(!isset($reply)){$reply = $from;}
if(!isset($fromName)){$fromName = $http_host;}
if(!isset($params)){
$params = array();
}elseif(!is_array($params)){
$params = json_decode($params, 1);
}
$modx->getService('mail', 'mail.modPHPMailer');
$pdoTools = $modx->getService('pdoTools');
$modx->getService('mail', 'mail.modPHPMailer');
$message = $pdoTools->getChunk($chunk, $params);
$modx->mail->set(modMail::MAIL_BODY,$message);
$modx->mail->set(modMail::MAIL_FROM, $from);
$modx->mail->set(modMail::MAIL_FROM_NAME, $fromName);
$modx->mail->set(modMail::MAIL_SUBJECT, $subject);
foreach($to as $t){
$modx->mail->address('to',$t);
}
$modx->mail->address('reply-to', $reply);
if(isset($attachment)){
$modx->mail->attach($attachment);
}
$modx->mail->setHTML(true);
if (!$modx->mail->send()) {
$modx->log(1,'При отправке письма произошла ошибка: '.$modx->mail->mailer->ErrorInfo);
return false;
}
$modx->mail->reset();
return true;На этом формирование и отправка билетов закончены, но нужно ещё написать плагин на событие msOnSubmitOrder, который будет проверять есть ли запрошенное пользователем количество билетов или нет. При этом админу доступен резерв.
<?php
if($order_id = (int)$_POST['id']){
$product = $modx->getObject('modResource', $order_id);
if($product){
$maxCount = $product->getTVValue('max_count');
$reservedCount = $product->getTVValue('reserved_count') ?: 0;
if($_POST['user_id']){
$availableCount = $maxCount;
}
else{
$availableCount = $maxCount - $reservedCount;
}
$sql = 'SELECT COUNT(id) FROM modxwb_ms2_order_products WHERE product_id = '.$order_id;
$statement = $modx->query($sql);
$result = $statement->fetchAll(PDO::FETCH_COLUMN);
$count = $result[0];
if(($count + $_POST['total_count']) > $availableCount){
$modx->event->output('Осталось билетов: ' . ($availableCount - $count));
}
if(($count + $_POST['total_count']) == $availableCount){
$product->set('published', 0);
$product->save();
}
}
}Осталось создать страницу для считывания qr-кодов и их проверки. HTML такой
<!-- styles это подключать не обязательно -->
<link rel="stylesheet" href="assets/project_files/libs/node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/project_files/libs/node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="assets/project_files/css/style.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<div class="container py-4">
{if $_modx->user.id > 0}
<div class="row">
<h1 class="col-12 text-center">{$pagetitle}</h1>
<div class="col-12">
<div id="loadingMessage">Unable to access video stream (please make sure you have a webcam enabled)</div>
<div class="text-center mb-2">
<canvas id="canvas" hidden style="height:250px;width:250px"></canvas>
</div>
<div id="info" class="alert alert-info d-none" role="alert">
<div class="d-flex flex-column">
<h4>Проверяем данные:</h4>
<p id="result"></p>
<p>
<em>Номер билета</em>
<span id="name"></span>
</p>
<p>
<em> Дата и время проведения</em>
<span id="event_date"></span>
</p>
<p>
<em>ФИО заказчика</em>
<span id="receiver"></span>
</p>
<p>
<em>Телефон заказчика</em>
<span id="phone"></span>
</p>
<p>
<em>Email заказчика</em>
<span id="email"></span>
</p>
<p id="id" style="position: absolute;opacity: 0;"></p>
<input type="hidden" id="user_id" value="{$_modx->user.id}">
</div>
</div>
</div>
</div>
{else}
<div class="row" id="error">
<h1 class="col-12 text-center">У вас нет прав на активацию</h1>
</div>
{/if}
</div>
<script src="assets/project_files/js/jsqr.js"></script> <!-- https://art-sites.ru/assets/art-sites/shared/jsqr.js -->
<script src="assets/project_files/js/common.js"></script> <!-- этот скрипт есть выше -->
<script src="assets/project_files/js/scanqr.js"></script> <!-- https://art-sites.ru/assets/art-sites/shared/scanqr.js -->Выводить информацию о билете можно по желанию, главное показывать результат проверки. Сам скрипт проверки находится в файле ajaxReceiver.php.БОНУС сниппет для проверки доступности покупки билетов на фронте, надо же сообщить что билеты проданы.
<?php
$product = $modx->getObject('msProduct', array('template' => 3, 'published' => 1));
if ($product) {
$event_date = strtotime($product->getTVValue('event_date'));
$now = time();
$diff = ($event_date - $now) / 60;
if ($diff <= 30) { //прекращаем продажу за полчаса до начала
$product->set('published', 0);
$product->save();
return false;
}
else {
$curCount = $modx->getCount('msOrderProducts', array('product_id' => $product->get('id')));
$maxCount = $product->getTVValue('max_count');
$availableCount = $maxCount;
$reservedCount = $product->getTVValue('reserved_count') ?: 0;
if (!$user_id) { //не позволяем обычному пользователю купить зарезервированные билеты.
$availableCount = $maxCount - $reservedCount;
}
if ($curCount >= $availableCount) {
return false;
}
else {
return true;
}
}
}UPD: решил ещё выложить шаблон из которого формируется pdf. В нём примечательно то, что никакие размеры кроме тех, что заданы в мм не работают для задания отступов. Второй момент, отступы сверху удалось задать только через padding, а слева только через margin.<h1>
{$name | replace: 'Билет ' : ''}
</h1>
<img src="{$qr}" alt="{$name}" width="200" height="200">
<style>
@page {
margin-bottom: 0;
margin-top: 0;
margin-left: 0;
margin-right: 0;
}
img{
margin-left: 130mm;
padding-top: 5mm;
}
h1 {
margin-left: 135mm;
padding-top: 15mm;
font-size: 26px;
color: #3f4d62;
}
</style>На этом всё! Комментарии: 12
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
1. «Чтобы избежать повторных отправок, записываем id заказа в куки… » — лучше таки флаг отправки сохранить в БД, например в properties msOrder. Ибо на страницу domain.ru?msorder=1 можно перейти не раз + она запросто может попасть в индекс яндекса (проверено) и будет весело. Запретите индексацию таких страниц в robots.
2. Откликаться будет ТОЛЬКО на ajax запросы: if ($_SERVER['REQUEST_METHOD'] != 'POST')…
Этот код будет работать при любом запросе к данной странице постом.
Меняйте на что-то вроде:
3. Отправку писем нужно повесить на событие оформления заказа или иное подходящее.
И я бы такой функционал в принципе не вязал бы с MS2, он тут по факту не нужен )
За публикацию личного опыта — спасибо.
Вангую немного на будущее:
— вдруг проект вырастет?
— вдруг выяснится, что народ закрывает страницу не дожидаясь отправки и не получает билеты, а потом жалуется?
— вдруг сбой отправки и вы об этом не узнаете, а повторной отправки не предусмотрено?
— вдруг надо будет ещё что-то делать вместе с отправкой билета?
Это я к тому, что лучше этот процесс перенести полностью на бэк, где вы его будете контролировать.
Чтобы БД не запрашивать (хотя для небольших проектов это не имеет особого значения, да и вообще — кому нужна БД — узкое место же :-) ) — можно в файлы json писать: есть файл -> читаем, получаем указанный заказ, отправляем, удаляем. Если отправка каких-то писем не удалась — пишем в лог и данный файл не удаляем. Однако, имхо, это немного извращение.
Подзаработать потом, это хорошо; главное, чтобы проект не возвращался на доработку неожиданно, как будет что-то всплывать. А то техдолг накопится и через пару лет работа будет ради работы делаться. Ну или работать по принципу «сдал проект — и меня нет», тоже так себе подход )))
Ну это прям призыв, чтобы пришли люди и грохнули сайт без каких-либо сложностей))
И даже так (и вроде когда используется массив) (обсуждали уже где-то здесь) что-угодно можно пропихнуть.
Всегда когда работаем с целыми числами всего лишь нужно добавить (int) перед чем-угодно и спать спокойно.
А это открывает доступ к другим типам инъекций.
Возможно, Fenom что-то там и порежет, но надеяться не стоит.