766 lines
34 KiB
PHP
766 lines
34 KiB
PHP
<?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;
|
||
|
||
// 商品列表查询钩子 — 注入坐标城市筛选
|
||
case 'plugins_service_goods_list_begin':
|
||
$ret = $this->OnGoodsListBegin($params);
|
||
break;
|
||
|
||
// 搜索列表查询钩子 — 注入坐标城市筛选
|
||
case 'plugins_service_search_goods_list_begin':
|
||
// [调试代码] 如需调试钩子触发,可取消下方注释:
|
||
// $debugFile = defined('ROOT') ? ROOT . 'runtime/debug_vr.log' : '/tmp/debug_vr.log';
|
||
// @file_put_contents($debugFile, date('Y-m-d H:i:s') . " switch: plugins_service_search_goods_list_begin hit!\n", FILE_APPEND);
|
||
$ret = $this->OnSearchListBegin($params);
|
||
break;
|
||
|
||
// 搜索结果后处理钩子 — 票务商品按 coding 合并
|
||
case 'plugins_service_search_goods_list_result':
|
||
$ret = $this->OnSearchListResult($params);
|
||
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分钟,已停止售票,请选择其他场次',
|
||
];
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────
|
||
// Step 3: 观影人信息校验(新增)
|
||
// ──────────────────────────────────────────────────────
|
||
// 1. 获取前端提交的 viewer_data
|
||
$viewerData = $params['data']['viewer_data'] ?? [];
|
||
if (!is_array($viewerData)) {
|
||
$viewerData = [];
|
||
}
|
||
|
||
// 2. 获取商品配置中的观影人要求
|
||
$viewerConfig = null;
|
||
foreach ($goodsItems as $item) {
|
||
$gid = intval($item['goods_id'] ?? 0);
|
||
if ($gid <= 0) continue;
|
||
|
||
// 只查询票务商品
|
||
$isTicketGood = isset($ticketGoodsMap[$gid]) && $ticketGoodsMap[$gid]['has_config'];
|
||
if (!$isTicketGood) continue;
|
||
|
||
$goods = Db::name('Goods')->where('id', $gid)->field('vr_goods_config')->find();
|
||
if (!empty($goods['vr_goods_config'])) {
|
||
$config = json_decode($goods['vr_goods_config'], true);
|
||
if (is_array($config) && !empty($config[0]['viewer_config'])) {
|
||
$viewerConfig = $config[0]['viewer_config'];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 如果商品要求观影人信息,进行校验
|
||
if (!empty($viewerConfig) && !empty($viewerConfig['require_viewer'])) {
|
||
// 检查 viewer_data 是否存在且为数组
|
||
if (empty($viewerData)) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '请填写观影人信息',
|
||
];
|
||
}
|
||
|
||
// 阶段1:只支持单个观影人(require_viewer_per_seat=0)
|
||
$perSeat = !empty($viewerConfig['require_viewer_per_seat']) ? $viewerConfig['require_viewer_per_seat'] : 0;
|
||
if ($perSeat == 0) {
|
||
if (count($viewerData) != 1) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '当前商品只需填写一个观影人信息',
|
||
];
|
||
}
|
||
} else {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '暂不支持每个座位独立填写观影人',
|
||
];
|
||
}
|
||
|
||
// 4. 遍历校验每个观影人的字段
|
||
$viewer = $viewerData[0] ?? [];
|
||
|
||
// 手机号校验
|
||
if (!empty($viewerConfig['require_viewer_mobile'])) {
|
||
if (empty($viewer['mobile'])) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '请填写观影人手机号',
|
||
];
|
||
}
|
||
if (!preg_match('/^1[3-9]\d{9}$/', $viewer['mobile'])) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '手机号格式不正确',
|
||
];
|
||
}
|
||
}
|
||
|
||
// 姓名校验
|
||
if (!empty($viewerConfig['require_viewer_name'])) {
|
||
if (empty($viewer['name'])) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '请填写观影人姓名',
|
||
];
|
||
}
|
||
}
|
||
|
||
// 身份证号校验(阶段1预留)
|
||
if (!empty($viewerConfig['require_viewer_idcard'])) {
|
||
if (empty($viewer['idcard'])) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '请填写观影人身份证号',
|
||
];
|
||
}
|
||
if (!preg_match('/^\d{17}[\dXx]$/', $viewer['idcard'])) {
|
||
return [
|
||
'code' => -1,
|
||
'msg' => '身份证号格式不正确',
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 商品列表查询钩子 — 注入坐标城市筛选(仅vr_ticket票务商品)
|
||
* 触发位置:plugins_service_goods_list_begin
|
||
*
|
||
* 向下兼容:无 user_lng/user_lat 参数时,不添加任何筛选,返回全部商品
|
||
* 修复:只对票务商品(vr_goods_config 非空)应用城市筛选,非票务商品不受影响
|
||
*
|
||
* @param array &$params 钩子参数引用 ['hook_name','is_backend','params','where','field','order_by','m','n']
|
||
*/
|
||
public function OnGoodsListBegin(&$params)
|
||
{
|
||
// [调试代码] 如需调试此钩子,可取消下方注释:
|
||
// $debugFile = defined('ROOT') ? ROOT . 'runtime/debug_vr.log' : '/tmp/debug_vr.log';
|
||
// @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnGoodsListBegin called\n", FILE_APPEND);
|
||
|
||
$paramsArr = $params['params'] ?? [];
|
||
|
||
// 优先级1:显式传入的 city_id / cityid
|
||
$cityId = isset($paramsArr['city_id']) ? intval($paramsArr['city_id']) : 0;
|
||
if ($cityId <= 0) {
|
||
$cityId = isset($paramsArr['cityid']) ? intval($paramsArr['cityid']) : 0;
|
||
}
|
||
|
||
// 优先级2:从坐标反推城市ID
|
||
if ($cityId <= 0) {
|
||
$userLng = isset($paramsArr['user_lng']) ? floatval($paramsArr['user_lng']) : 0;
|
||
$userLat = isset($paramsArr['user_lat']) ? floatval($paramsArr['user_lat']) : 0;
|
||
|
||
if ($userLng != 0 && $userLat != 0) {
|
||
$cityId = \app\plugins\vr_ticket\service\GeoCityService::FindNearestCityId($userLng, $userLat);
|
||
}
|
||
}
|
||
|
||
// 向下兼容:无坐标无city_id → 不筛选,返回全部
|
||
if ($cityId <= 0) {
|
||
return;
|
||
}
|
||
|
||
// 修复:只对票务商品应用城市筛选
|
||
// 逻辑:(非票务商品 vr_goods_config='') OR (票务商品且城市匹配)
|
||
$where = &$params['where'];
|
||
if (!is_array($where)) {
|
||
$where = [];
|
||
}
|
||
|
||
// 使用闭包实现 OR 条件:vr_goods_config 为空(非票务) OR produce_region = 城市ID
|
||
$where[] = function($query) use ($cityId) {
|
||
$query->where(function($q) {
|
||
$q->where('g.vr_goods_config', '=', '');
|
||
})->whereOr(function($q) use ($cityId) {
|
||
$q->where('g.vr_goods_config', '<>', '')->where('g.produce_region', '=', $cityId);
|
||
});
|
||
};
|
||
// @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnGoodsListBegin: city filter applied to ticket goods only, cityId=$cityId\n", FILE_APPEND);
|
||
}
|
||
|
||
/**
|
||
* 搜索列表查询钩子 — 注入坐标城市筛选(仅vr_ticket票务商品)
|
||
* 触发位置:plugins_service_search_goods_list_begin
|
||
*
|
||
* 向下兼容:无 user_lng/user_lat 参数时,不添加任何筛选,返回全部商品
|
||
* 修复:只对票务商品(vr_goods_config 非空)应用城市筛选,非票务商品不受影响
|
||
*
|
||
* @param array &$params 钩子参数引用 ['hook_name','is_backend','params','where_base','where_keywords','where_screening_price','field','order_by','page','page_start','page_size']
|
||
*/
|
||
public function OnSearchListBegin(&$params)
|
||
{
|
||
// [调试代码] 如需调试此钩子,可取消下方注释:
|
||
// $debugFile = defined('ROOT') ? ROOT . 'runtime/debug_vr.log' : '/tmp/debug_vr.log';
|
||
// @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnSearchListBegin called\n", FILE_APPEND);
|
||
|
||
$paramsArr = $params['params'] ?? [];
|
||
|
||
// 优先级1:显式传入的 city_id / cityid
|
||
$cityId = isset($paramsArr['city_id']) ? intval($paramsArr['city_id']) : 0;
|
||
if ($cityId <= 0) {
|
||
$cityId = isset($paramsArr['cityid']) ? intval($paramsArr['cityid']) : 0;
|
||
}
|
||
|
||
// 优先级2:从坐标反推城市ID
|
||
if ($cityId <= 0) {
|
||
$userLng = isset($paramsArr['user_lng']) ? floatval($paramsArr['user_lng']) : 0;
|
||
$userLat = isset($paramsArr['user_lat']) ? floatval($paramsArr['user_lat']) : 0;
|
||
|
||
if ($userLng != 0 && $userLat != 0) {
|
||
$cityId = \app\plugins\vr_ticket\service\GeoCityService::FindNearestCityId($userLng, $userLat);
|
||
}
|
||
}
|
||
|
||
// 向下兼容:无坐标无city_id → 不筛选,返回全部
|
||
if ($cityId <= 0) {
|
||
// [调试代码] @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnSearchListBegin: no cityId found, cityId=$cityId\n", FILE_APPEND);
|
||
return;
|
||
}
|
||
|
||
// 修复:只对票务商品应用城市筛选
|
||
// 逻辑:(非票务商品 vr_goods_config='') OR (票务商品且城市匹配)
|
||
// SearchService 使用 where_base 而非 where
|
||
$whereBase = &$params['where_base'];
|
||
if (!is_array($whereBase)) {
|
||
$whereBase = [];
|
||
}
|
||
|
||
// 使用闭包实现 OR 条件:vr_goods_config 为空(非票务) OR produce_region = 城市ID
|
||
$whereBase[] = function($query) use ($cityId) {
|
||
$query->where(function($q) {
|
||
$q->where('g.vr_goods_config', '=', '');
|
||
})->whereOr(function($q) use ($cityId) {
|
||
$q->where('g.vr_goods_config', '<>', '')->where('g.produce_region', '=', $cityId);
|
||
});
|
||
};
|
||
// [调试代码] @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnSearchListBegin: city filter applied to ticket goods only, cityId=$cityId\n", FILE_APPEND);
|
||
}
|
||
|
||
/**
|
||
* 搜索结果后处理钩子 — 票务商品按 coding 合并
|
||
* 触发位置:plugins_service_search_goods_list_result
|
||
*
|
||
* 仅对 UniApp 端(H5 + 小程序)生效:
|
||
* - H5: h5
|
||
* - 小程序: weixin, alipay, baidu, toutiao, qq, kuaishou
|
||
* - App: ios, android
|
||
*
|
||
* 合并规则:以当前日期为准
|
||
* 1. 优先选择:最接近今天且 >= 今天 的那个
|
||
* 2. 备选:如果都过期了(< 今天),选择最接近今天的那一个
|
||
*
|
||
* @param array &$params 钩子参数引用 ['hook_name','is_backend','params','data','total']
|
||
*/
|
||
public function OnSearchListResult(&$params)
|
||
{
|
||
// 仅对 UniApp 端生效(排除 PC端)
|
||
$uniapp_types = ['h5', 'weixin', 'alipay', 'baidu', 'toutiao', 'qq', 'kuaishou', 'ios', 'android'];
|
||
if (!in_array(APPLICATION_CLIENT_TYPE, $uniapp_types)) {
|
||
return;
|
||
}
|
||
|
||
$data = &$params['data'];
|
||
if (empty($data) || !is_array($data)) {
|
||
return;
|
||
}
|
||
|
||
// 今天日期(用于比较)
|
||
$today = date('Y-m-d');
|
||
$todayTs = strtotime($today);
|
||
|
||
// 用于记录每个 coding 对应的票务商品
|
||
// 结构: $ticketCodingMap[coding] = ['item' => array, 'diff' => int]
|
||
// diff: 距今天的天数差(正值=未来,负值=过去)
|
||
$ticketCodingMap = [];
|
||
// 非票务商品直接保留
|
||
$nonTicketGoods = [];
|
||
|
||
foreach ($data as $item) {
|
||
$vrConfig = $item['vr_goods_config'] ?? '';
|
||
$coding = $item['coding'] ?? '';
|
||
|
||
if (!empty($vrConfig) && !empty($coding)) {
|
||
// 票务商品:按 coding 合并
|
||
$batchExpire = $item['batch_number_expire'] ?? '';
|
||
|
||
// 计算与今天的差距
|
||
$diff = self::CalcDateDiffFromToday($batchExpire);
|
||
|
||
if (!isset($ticketCodingMap[$coding])) {
|
||
// 第一个记录,直接保存
|
||
$ticketCodingMap[$coding] = [
|
||
'item' => $item,
|
||
'diff' => $diff,
|
||
];
|
||
} else {
|
||
// 比较优先级:
|
||
// 1. 优先选择 diff >= 0 中最小的(今天或未来的最近日期)
|
||
// 2. 如果没有 diff >= 0,选择 diff < 0 中绝对值最小的(最近的过期日期)
|
||
$existingDiff = $ticketCodingMap[$coding]['diff'];
|
||
|
||
if ($this->IsBetterDateCandidate($diff, $existingDiff)) {
|
||
$ticketCodingMap[$coding] = [
|
||
'item' => $item,
|
||
'diff' => $diff,
|
||
];
|
||
}
|
||
}
|
||
} else {
|
||
// 非票务商品直接保留
|
||
$nonTicketGoods[] = $item;
|
||
}
|
||
}
|
||
|
||
// 合并结果:非票务商品 + 合并后的票务商品
|
||
$mergedData = [];
|
||
foreach ($ticketCodingMap as $entry) {
|
||
$mergedData[] = $entry['item'];
|
||
}
|
||
foreach ($nonTicketGoods as $item) {
|
||
$mergedData[] = $item;
|
||
}
|
||
|
||
// 更新引用数据
|
||
$params['data'] = $mergedData;
|
||
$params['total'] = count($mergedData);
|
||
}
|
||
|
||
/**
|
||
* 判断新的日期差是否比现有的更好
|
||
*
|
||
* 优先级规则:
|
||
* 1. 优先选择 diff >= 0 中最小的(今天或未来的最近日期)
|
||
* 2. 如果没有 diff >= 0,选择 diff < 0 中绝对值最小的(最近的过期日期)
|
||
*
|
||
* @param int $newDiff 新日期与今天的差距
|
||
* @param int $existingDiff 现有日期与今天的差距
|
||
* @return bool true 表示新日期更好,应该替换
|
||
*/
|
||
private function IsBetterDateCandidate($newDiff, $existingDiff)
|
||
{
|
||
// 新日期是未来/今天,现有是过去 → 新更好
|
||
if ($newDiff >= 0 && $existingDiff < 0) {
|
||
return true;
|
||
}
|
||
|
||
// 新日期是过去,现有是未来/今天 → 现有更好
|
||
if ($newDiff < 0 && $existingDiff >= 0) {
|
||
return false;
|
||
}
|
||
|
||
// 同为未来/今天:选最小的(最近的未来日期)
|
||
if ($newDiff >= 0 && $existingDiff >= 0) {
|
||
return $newDiff < $existingDiff;
|
||
}
|
||
|
||
// 同为过去:选最大的(绝对值最小=最接近今天)
|
||
if ($newDiff < 0 && $existingDiff < 0) {
|
||
return $newDiff > $existingDiff;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 计算日期与今天的差距(天数)
|
||
*
|
||
* @param string $dateStr 日期字符串 "Y-m-d" 格式
|
||
* @return int 天数差(正值=未来,负值=过去),空值返回 INF(最大正数,视为最远)
|
||
*/
|
||
private static function CalcDateDiffFromToday($dateStr)
|
||
{
|
||
if (empty($dateStr)) {
|
||
return PHP_INT_MAX; // 空日期视为最远,不优先选择
|
||
}
|
||
|
||
$dateTs = strtotime($dateStr);
|
||
if ($dateTs === false) {
|
||
return PHP_INT_MAX;
|
||
}
|
||
|
||
$todayTs = strtotime(date('Y-m-d'));
|
||
return round(($dateTs - $todayTs) / 86400);
|
||
}
|
||
}
|
||
?>
|