vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/Hook.php

387 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace app\plugins\vr_ticket;
require_once __DIR__ . '/service/BaseService.php';
use app\plugins\vr_ticket\service\BaseService;
use app\plugins\vr_ticket\service\TicketService;
use think\facade\Db;
class Hook
{
public function handle($params = [])
{
if(!empty($params['hook_name']))
{
$ret = '';
switch($params['hook_name'])
{
// 后台左侧菜单钩子
case 'plugins_service_admin_menu_data':
$this->AdminSidebarInit($params['admin_left_menu']);
break;
// 订单支付成功处理
case 'plugins_service_order_pay_success_handle_end':
BaseService::log('Hook::handle triggered', ['order_id' => $params['order_id'] ?? $params['business_id'] ?? 'unknown'], 'info');
$ret = TicketService::onOrderPaid($params);
BaseService::log('Hook::handle result', ['ret' => $ret], 'info');
break;
case 'plugins_service_order_detail_page_info':
// C端订单详情页注入票夹入口
$ret = $this->InjectTicketCard($params);
break;
case 'plugins_view_user_various_bottom':
// C端用户中心底部挂载票夹入口
$ret = $this->InjectWalletLink($params);
break;
case 'plugins_service_buy_order_insert_begin':
// C端下单前校验距离开场不足5分钟禁止购买
$ret = $this->BuyCheck($params);
break;
case 'plugins_service_order_delete_success':
// 如果有删除拦截等
break;
}
return $ret;
}
}
public function AdminSidebarInit(&$params)
{
$params[] = [
'id' => 'plugins-vr_ticket',
'name' => 'VR票务',
'title' => 'VR票务',
'icon' => 'am-icon-ticket',
'control' => 'admin',
'action' => 'index',
'is_show' => 1,
'power' => 'vr_ticket-admin',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'index'),
'items' => [
[
'id' => 'plugins-vr_ticket-venue',
'name' => '场馆配置',
'title' => '场馆配置',
'is_show' => 1,
'control' => 'admin',
'action' => 'VenueList',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'VenueList'),
'power' => 'vr_ticket-venueList',
],
[
'id' => 'plugins-vr_ticket-ticket',
'name' => '电子票',
'title' => '电子票',
'is_show' => 1,
'control' => 'admin',
'action' => 'TicketList',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'TicketList'),
'power' => 'vr_ticket-ticketList',
],
[
'id' => 'plugins-vr_ticket-ticketverify',
'name' => '扫码核销',
'title' => '扫码核销',
'is_show' => 1,
'control' => 'admin',
'action' => 'TicketVerify',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'TicketVerify'),
'power' => 'vr_ticket-ticketVerify',
],
[
'id' => 'plugins-vr_ticket-verifier',
'name' => '核销员',
'title' => '核销员',
'is_show' => 1,
'control' => 'admin',
'action' => 'VerifierList',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'VerifierList'),
'power' => 'vr_ticket-verifierList',
],
[
'id' => 'plugins-vr_ticket-varification',
'name' => '核销记录',
'title' => '核销记录',
'is_show' => 1,
'control' => 'admin',
'action' => 'VerificationList',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'VerificationList'),
'power' => 'vr_ticket-verificationList',
],
[
'id' => 'plugins-vr_ticket-setup',
'name' => '插件设置',
'title' => '插件设置',
'is_show' => 1,
'control' => 'admin',
'action' => 'Setup',
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'Setup'),
'power' => 'vr_ticket-setup',
]
]
];
}
/**
* C端订单详情页注入票卡片
*/
public function InjectTicketCard(&$params)
{
$order = $params['order'] ?? [];
if (empty($order) || ($order['pay_status'] ?? 0) != 1) {
return;
}
// 获取当前登录用户ShopXO 标准方式)
$user = \app\service\UserService::LoginUserInfo();
$userId = empty($user) ? null : $user['id'];
if (empty($userId)) {
return;
}
$tickets = \think\facade\Db::name('vr_tickets')
->where('order_id', $order['id'])
->select()
->toArray();
if (empty($tickets)) {
return;
}
$token = session('user_token') ?: '';
$hostUrl = \think\facade\Config::get('shopxo.host_url');
$ticketCardsHtml = '';
foreach ($tickets as $ticket) {
$shortCode = \app\plugins\vr_ticket\service\BaseService::shortCodeEncode($ticket['goods_id'], $ticket['id']);
$statusMap = [0 => ['text' => '未核销', 'class' => 'unverified'], 1 => ['text' => '已核销', 'class' => 'verified'], 2 => ['text' => '已退款', 'class' => 'refunded']];
$status = $statusMap[$ticket['verify_status']] ?? $statusMap[0];
$ticketCardsHtml .= '<div class="vr-ticket-card" data-ticket-id="' . $ticket['id'] . '">' .
'<div class="vr-ticket-card-header">' .
'<div class="vr-ticket-goods-title">电子票</div>' .
'<div class="vr-ticket-status ' . $status['class'] . '">' . $status['text'] . '</div>' .
'</div>' .
'<div class="vr-ticket-info">' .
'<div class="vr-ticket-info-row"><span class="vr-ticket-info-icon">💺</span><span>' . htmlspecialchars($ticket['seat_info'] ?? '') . '</span></div>' .
'<div class="vr-ticket-info-row"><span class="vr-ticket-info-icon">👤</span><span>' . htmlspecialchars($ticket['real_name'] ?? '') . '</span></div>' .
'</div>' .
'<div class="vr-ticket-footer">' .
'<div class="vr-ticket-short-code">短码: ' . htmlspecialchars($shortCode) . '</div>' .
'<a href="javascript:;" class="vr-ticket-view-btn" onclick="VrTicketWallet.viewTicket(' . $ticket['id'] . ')">查看票码 →</a>' .
'</div>' .
'</div>';
}
$style = '<style>
.vr-ticket-card { background: #fff; border-radius: 12px; padding: 16px; margin: 12px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.06); cursor: pointer; }
.vr-ticket-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
.vr-ticket-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.vr-ticket-goods-title { font-size: 16px; font-weight: 600; color: #333; }
.vr-ticket-status { font-size: 12px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
.vr-ticket-status.unverified { background: #e6f7ff; color: #1890ff; }
.vr-ticket-status.verified { background: #f6ffed; color: #52c41a; }
.vr-ticket-status.refunded { background: #fff1f0; color: #ff4d4f; }
.vr-ticket-info { font-size: 13px; color: #666; line-height: 1.6; }
.vr-ticket-info-row { display: flex; align-items: center; margin-bottom: 4px; }
.vr-ticket-info-icon { width: 16px; color: #999; margin-right: 6px; }
.vr-ticket-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
.vr-ticket-short-code { font-size: 14px; font-family: "Courier New", monospace; color: #333; font-weight: 600; letter-spacing: 1px; }
.vr-ticket-view-btn { font-size: 13px; color: #1890ff; text-decoration: none; }
.vr-ticket-view-btn:hover { text-decoration: underline; }
</style>';
$ticketHtml = '<div class="vr-order-ticket-section">' .
'<div style="font-size:16px;font-weight:600;margin-bottom:12px;">📋 我的电子票</div>' .
$ticketCardsHtml .
'</div>';
$params['page_data']['ticket_section'] = $ticketHtml;
$params['page_data']['ticket_css'] = $style;
// JS
$js = '<script>
(function() {
var apiBase = "' . $hostUrl . '/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=";
var token = "' . htmlspecialchars($token) . '";
window.VrTicketWallet = {
viewTicket: function(ticketId) {
var modal = document.getElementById("vrTicketModal") || createModal();
modal.classList.add("active");
var body = document.getElementById("vrTicketModalBody");
body.innerHTML = \'<div style="text-align:center;padding:40px;">加载中...</div>\';
$.ajax({
url: apiBase + "detail&id=" + ticketId,
headers: token ? {"X-Token": token} : {},
success: function(res) {
if (res.code === 0 && res.data.ticket) {
var t = res.data.ticket;
var statusMap = {0:{text:"未核销",class:"unverified"},1:{text:"已核销",class:"verified"},2:{text:"已退款",class:"refunded"}};
var status = statusMap[t.verify_status] || statusMap[0];
body.innerHTML = \'<div style="text-align:center;padding:20px;background:#fafafa;border-radius:12px;"><div id="vrQrcodeBox"></div></div>\' +
\'<div style="text-align:center;margin:16px 0;padding:12px;background:#f5f5f5;border-radius:8px;">\' +
\'<div style="font-size:12px;color:#999;margin-bottom:4px;">短码(人工核销)</div>\' +
\'<div style="font-size:20px;font-family:monospace;font-weight:700;letter-spacing:2px;">\' + t.short_code + \'</div></div>\' +
\'<div style="text-align:center;"><span class="vr-ticket-status \' + status.class + \'">\' + status.text + \'</span></div>\';
if (t.qr_payload) {
$("#vrQrcodeBox").qrcode({text: atob(t.qr_payload), width: 180, height: 180});
}
}
}
});
},
closeModal: function() {
var modal = document.getElementById("vrTicketModal");
if (modal) modal.classList.remove("active");
}
};
function createModal() {
var html = \'<div id="vrTicketModal" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:none;align-items:center;justify-content:center;">\' +
\'<div style="background:#fff;border-radius:16px;width:90%;max-width:400px;padding:24px;"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">\' +
\'<div style="font-size:18px;font-weight:600;">电子票</div><button onclick="VrTicketWallet.closeModal()" style="width:28px;height:28px;border-radius:50%;background:#f0f0f0;border:none;cursor:pointer;">×</button></div>\' +
\'<div id="vrTicketModalBody"></div></div></div>\';
document.body.insertAdjacentHTML("beforeend", html);
var modal = document.getElementById("vrTicketModal");
modal.addEventListener("click", function(e) { if (e.target === modal) VrTicketWallet.closeModal(); });
return modal;
}
})();
</script>';
$params['page_data']['ticket_js'] = $js;
}
/**
* 在用户中心底部挂载票夹入口链接
*/
/**
* C端下单前校验票务商品数据完整性 + 停售时间校验
*
* 两层校验(仅针对票务商品):
* 1. 数据完整性batch_number_expire 必须 > 0演出日期必填否则拒绝下单
* 2. 时效性batch_expire_ts演出前5分钟截止已过期则拒绝下单
*
* @param array $params 钩子参数 ['hook_name' => ..., 'data' => ..., 'order' => ..., 'goods' => ...]
* @return array ['code' => 0|非0, 'msg' => ...]
*/
public function BuyCheck(&$params)
{
$goodsItems = $params['goods'] ?? [];
if (empty($goodsItems)) {
return ['code' => 0, 'msg' => ''];
}
// ──────────────────────────────────────────────────────
// Step 1: 批量查询票务商品标识
// 提取所有 goods_id一次性查询 vr_goods_config 非空的商品
// 构建 ticketGoodsMap {goods_id => has_config}
// ──────────────────────────────────────────────────────
$goodsIds = [];
foreach ($goodsItems as $item) {
$gid = intval($item['goods_id'] ?? 0);
if ($gid > 0) {
$goodsIds[] = $gid;
}
}
$goodsIds = array_unique($goodsIds);
$ticketGoodsMap = []; // goods_id => has_config (bool)
if (!empty($goodsIds)) {
$ticketGoods = Db::name('Goods')
->where('id', 'in', $goodsIds)
->where('vr_goods_config', '<>', '')
->field('id, batch_number_expire')
->select()
->toArray();
foreach ($ticketGoods as $tg) {
$ticketGoodsMap[$tg['id']] = [
'has_config' => true,
'batch_number_expire' => intval($tg['batch_number_expire'] ?? 0),
];
}
}
// ──────────────────────────────────────────────────────
// Step 2: 逐商品校验
// ──────────────────────────────────────────────────────
$now = time();
foreach ($goodsItems as $item) {
$gid = intval($item['goods_id'] ?? 0);
$isTicketGood = isset($ticketGoodsMap[$gid]) && $ticketGoodsMap[$gid]['has_config'];
// 非票务商品:跳过
if (!$isTicketGood) {
continue;
}
// --- 校验 1: batch_number_expire 数据完整性 ---
// 优先用 DB 查询结果(最新);其次用订单 item 中的值
$dbBatchExpire = $ticketGoodsMap[$gid]['batch_number_expire'] ?? 0;
if ($dbBatchExpire <= 0) {
$itemTitle = $item['title'] ?? ('商品#' . $gid);
return [
'code' => -1,
'msg' => '「' . $itemTitle . '」未设置演出日期,暂时无法购买',
];
}
// --- 校验 2: batch_expire_ts 停售时间 ---
// 从 SKU extends 读取SeatSkuService::BatchGenerate 已写入)
$extends = $item['extends'] ?? [];
if (empty($extends) || !is_array($extends)) {
if (!empty($item['extends']) && is_string($item['extends'])) {
$extends = json_decode($item['extends'], true);
}
if (empty($extends) || !is_array($extends)) {
continue; // 无 extends 数据,跳过停售校验(放过)
}
}
if (!isset($extends['batch_expire_ts'])) {
continue; // 未设置停售时间,跳过
}
$batchExpireTs = intval($extends['batch_expire_ts']);
if ($batchExpireTs <= 0) {
continue;
}
if ($now >= $batchExpireTs) {
$sessionInfo = '';
if (!empty($extends['session_start']) && !empty($extends['session_end'])) {
$sessionInfo = '' . $extends['session_start'] . ' - ' . $extends['session_end'] . '';
}
return [
'code' => -1,
'msg' => '该场次' . $sessionInfo . '距开场已不足5分钟已停止售票请选择其他场次',
];
}
}
return ['code' => 0, 'msg' => ''];
}
public function InjectWalletLink(&$params)
{
$hostUrl = \think\facade\Config::get('shopxo.host_url');
// 票夹入口 HTML - 直接返回 HTML 字符串
// 正确的插件路由格式:?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=index&pluginsaction=wallet
$walletLink = '<div class="vr-wallet-entrance" style="margin-top:20px;">' .
'<a href="' . $hostUrl . '?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=index&pluginsaction=wallet" ' .
'style="display:inline-flex;align-items:center;padding:12px 20px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);' .
'color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;box-shadow:0 4px 12px rgba(102,126,234,0.3);">' .
'<span style="font-size:18px;margin-right:8px;">🎫</span> 我的电子票' .
'</a></div>';
return $walletLink;
}
}
?>