[СДЕЛАЙ САМ] Генерация, вставка в 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')…
Этот код будет работать при любом запросе к данной странице постом.
Меняйте на что-то вроде:
И я бы такой функционал в принципе не вязал бы с MS2, он тут по факту не нужен )
За публикацию личного опыта — спасибо.
1. «Чтобы избежать повторных отправок, записываем id заказа в куки… » — лучше таки флаг отправки сохранить в БД, например в properties msOrder. Ибо на страницу domain.ru?msorder=1 можно перейти не раз + она запросто может попасть в индекс яндекса (проверено) и будет весело. Запретите индексацию таких страниц в robots.
2. Откликаться будет ТОЛЬКО на ajax запросы: if ($_SERVER['REQUEST_METHOD'] != 'POST')…
Этот код будет работать при любом запросе к данной странице постом.
Меняйте на что-то вроде:
if(empty($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') die;
3. Отправку писем нужно повесить на событие оформления заказа или иное подходящее.И я бы такой функционал в принципе не вязал бы с MS2, он тут по факту не нужен )
За публикацию личного опыта — спасибо.
Спасибо за комментарии. По первым двум пунктам спорить не буду, особенно по второму. Что касается третьего, изначально так и было, однако отправка письма с вложением при покупке разом больше одного билета вызывала зависание от 3 секунд, возможно это связано с логикой работы ms2, поэтому я решил данный функционал вынести на отдельную страницу, потому как пользователь на неё в любом случае переходит и там он уже ничего не заметит. Если будет сбоить, повешу задачу в крон. А что до ms2 и его нужности, будь к меня скилл повыше, наверное, я его бы не использовал, но с другой стороны ms2 дал почти весь функционал, я ведь совсем немного логику подправил и генерацию билета прикрутил и всё, к тому же ещё оплату подключать, а для ms2 уже есть модули.
Да, именно на крон. Сохраняем очередь отправки в отдельную таблицу, потом кроном раз в ~ 1 мин отправляем накопившиеся и удаляем из таблицы. Лимит на отправку за один запуск скрипта ~ 5-10 писем, чтобы не иметь проблем с хостером (чтобы за спам 100% не принимали).
Понимаете какое дело, бо́льшую часть времени крон будет работать в холостую. Получается он должен раз в минуту, пусть даже раз в пять минут запускать скрипт который будет делать запрос в БД, и если есть письмо, отправлять, если нет, то ничего не делать. Но мероприятия маленькие до 500-600 человек, это примерно по 1 письму в час, если продажу начинать за месяц, в лучшем случае. В общем у меня нет каких-то обоснованных аргументов, но сам факт того что скрипт будет срабатывать впустую мне не нравится))) Я лучше пятисекудный таймер добавлю, чтобы ajax точно скрипт запустил типа «Отправляем билеты. Ждите 5...4...3...2...1 Билеты отправлены. До скорой встречи!»)))
Ну да, как вариант — конкретная реализация от задачи.
Вангую немного на будущее:
— вдруг проект вырастет?
— вдруг выяснится, что народ закрывает страницу не дожидаясь отправки и не получает билеты, а потом жалуется?
— вдруг сбой отправки и вы об этом не узнаете, а повторной отправки не предусмотрено?
— вдруг надо будет ещё что-то делать вместе с отправкой билета?
Это я к тому, что лучше этот процесс перенести полностью на бэк, где вы его будете контролировать.
Чтобы БД не запрашивать (хотя для небольших проектов это не имеет особого значения, да и вообще — кому нужна БД — узкое место же :-) ) — можно в файлы json писать: есть файл -> читаем, получаем указанный заказ, отправляем, удаляем. Если отправка каких-то писем не удалась — пишем в лог и данный файл не удаляем. Однако, имхо, это немного извращение.
Вангую немного на будущее:
— вдруг проект вырастет?
— вдруг выяснится, что народ закрывает страницу не дожидаясь отправки и не получает билеты, а потом жалуется?
— вдруг сбой отправки и вы об этом не узнаете, а повторной отправки не предусмотрено?
— вдруг надо будет ещё что-то делать вместе с отправкой билета?
Это я к тому, что лучше этот процесс перенести полностью на бэк, где вы его будете контролировать.
Чтобы БД не запрашивать (хотя для небольших проектов это не имеет особого значения, да и вообще — кому нужна БД — узкое место же :-) ) — можно в файлы json писать: есть файл -> читаем, получаем указанный заказ, отправляем, удаляем. Если отправка каких-то писем не удалась — пишем в лог и данный файл не удаляем. Однако, имхо, это немного извращение.
Вечно эти опытные программисты находят изъяны и не дают насладиться маленьким триумфом от собственного роста? Я понял вас, спасибо. Что до повторной отправки, можно предусмотреть: пишем в поле comment ссылку domain.ru?msorder=2 и выводим это поле в таблице заказов. Логгирование ошибок в скрипте отправки есть, надо только возвращать false и наверное уведомлять админа. Про чтение json это прям сильно))) А вот когда вырастет, тогда можно будет переделать и ещё немного заработать ?
Опыт — сын ошибок трудных (©), делюсь, пока минута есть.
Подзаработать потом, это хорошо; главное, чтобы проект не возвращался на доработку неожиданно, как будет что-то всплывать. А то техдолг накопится и через пару лет работа будет ради работы делаться. Ну или работать по принципу «сдал проект — и меня нет», тоже так себе подход )))
Подзаработать потом, это хорошо; главное, чтобы проект не возвращался на доработку неожиданно, как будет что-то всплывать. А то техдолг накопится и через пару лет работа будет ради работы делаться. Ну или работать по принципу «сдал проект — и меня нет», тоже так себе подход )))
Нет, я не приемлю принцип «сдал и меня нет», я поддерживаю свои проекты. И стараюсь заранее все нюансы у заказчика узнать, чтобы доработок было минимум. Понятно, что потом аппетиты могут у заказчика вырасти, тогда и переделаем. А за то, что опытом делитесь респект!
Вроде к каждой статье пишут про безопасность, не?)
Всегда когда работаем с целыми числами всего лишь нужно добавить (int) перед чем-угодно и спать спокойно.
А это открывает доступ к другим типам инъекций.
$sql = 'SELECT COUNT(id) FROM modxwb_ms2_order_products WHERE product_id = '.$_POST['id'];
Ну это прям призыв, чтобы пришли люди и грохнули сайт без каких-либо сложностей))$modx->getObject('modResource', $_POST['id']);
И даже так (и вроде когда используется массив) (обсуждали уже где-то здесь) что-угодно можно пропихнуть. Всегда когда работаем с целыми числами всего лишь нужно добавить (int) перед чем-угодно и спать спокойно.
А это открывает доступ к другим типам инъекций.
<input type="hidden" id="orderid" value="{$.get.msorder}">
Возможно, Fenom что-то там и порежет, но надеяться не стоит.
Спасибо за уделенное время. Исправления внёс:-)
Пожалуйста, только тут лучше (float), иначе уже бизнес логика может пострадать))
(int)$_POST['price']
Увлёкся)))
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.