2026-04-15 05:09:44 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* VR票务插件 - 基础服务
|
|
|
|
|
|
*
|
|
|
|
|
|
* @package vr_ticket\service
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\plugins\vr_ticket\service;
|
|
|
|
|
|
|
|
|
|
|
|
class BaseService
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取插件表前缀
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function table($name)
|
|
|
|
|
|
{
|
|
|
|
|
|
return 'plugins_vr_' . $name;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function now()
|
|
|
|
|
|
{
|
|
|
|
|
|
return time();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成 UUID v4 票码
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function generateUuid()
|
|
|
|
|
|
{
|
|
|
|
|
|
$data = random_bytes(16);
|
|
|
|
|
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
|
|
|
|
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
|
|
|
|
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* AES-256-CBC 加密 QR 数据
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $data 待加密数据
|
|
|
|
|
|
* @param int|null $expire 过期时间戳(默认30天)
|
|
|
|
|
|
* @return string base64 编码密文
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function encryptQrData($data, $expire = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
$secret = self::getQrSecret();
|
|
|
|
|
|
$expire = $expire ?? (time() + 86400 * 30);
|
|
|
|
|
|
|
|
|
|
|
|
$payload = json_encode(array_merge($data, [
|
|
|
|
|
|
'exp' => $expire,
|
|
|
|
|
|
'iat' => time(),
|
|
|
|
|
|
]), JSON_UNESCAPED_UNICODE);
|
|
|
|
|
|
|
|
|
|
|
|
$iv = random_bytes(16);
|
|
|
|
|
|
$encrypted = openssl_encrypt($payload, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);
|
|
|
|
|
|
|
|
|
|
|
|
return base64_encode($iv . $encrypted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解密 QR 数据
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $encoded base64 编码密文
|
|
|
|
|
|
* @return array|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function decryptQrData($encoded)
|
|
|
|
|
|
{
|
|
|
|
|
|
$secret = self::getQrSecret();
|
|
|
|
|
|
$combined = base64_decode($encoded);
|
|
|
|
|
|
|
|
|
|
|
|
if (strlen($combined) < 16) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$iv = substr($combined, 0, 16);
|
|
|
|
|
|
$encrypted = substr($combined, 16);
|
|
|
|
|
|
|
|
|
|
|
|
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);
|
|
|
|
|
|
|
|
|
|
|
|
if ($decrypted === false) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$data = json_decode($decrypted, true);
|
|
|
|
|
|
|
|
|
|
|
|
if (isset($data['exp']) && $data['exp'] < time()) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 QR 加密密钥
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static function getQrSecret()
|
|
|
|
|
|
{
|
|
|
|
|
|
$secret = env('VR_TICKET_QR_SECRET', '');
|
2026-04-15 08:59:22 +00:00
|
|
|
|
if (empty($secret)) {
|
|
|
|
|
|
throw new \Exception('[vr_ticket] VR_TICKET_QR_SECRET 环境变量未配置,QR加密密钥不能为空。请在.env中设置VR_TICKET_QR_SECRET=<随机64字符字符串>');
|
2026-04-15 05:09:44 +00:00
|
|
|
|
}
|
2026-04-15 08:59:22 +00:00
|
|
|
|
return $secret;
|
2026-04-15 05:09:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 判断商品是否为票务商品
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $goods_id
|
|
|
|
|
|
* @return bool
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function isTicketGoods($goods_id)
|
|
|
|
|
|
{
|
|
|
|
|
|
$goods = \Db::name('Goods')->find($goods_id);
|
|
|
|
|
|
if (empty($goods)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return !empty($goods['venue_data']) || ($goods['item_type'] ?? '') === 'ticket';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取商品座位模板
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $goods_id
|
|
|
|
|
|
* @return array|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function getSeatTemplateByGoods($goods_id)
|
|
|
|
|
|
{
|
|
|
|
|
|
$goods = \Db::name('Goods')->find($goods_id);
|
|
|
|
|
|
if (empty($goods) || empty($goods['category_id'])) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return \Db::name(self::table('seat_templates'))
|
|
|
|
|
|
->where('category_id', $goods['category_id'])
|
|
|
|
|
|
->where('status', 1)
|
|
|
|
|
|
->find();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 安全日志
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function log($message, $context = [], $level = 'info')
|
|
|
|
|
|
{
|
|
|
|
|
|
$tag = '[vr_ticket]';
|
|
|
|
|
|
$ctx = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
|
|
|
|
|
|
$log_func = "log_{$level}";
|
|
|
|
|
|
if (function_exists($log_func)) {
|
|
|
|
|
|
$log_func($tag . $message . $ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 插件后台权限菜单
|
|
|
|
|
|
*
|
|
|
|
|
|
* ShopXO 通过 PluginsService::PluginsAdminPowerMenu() 调用此方法
|
|
|
|
|
|
* 返回格式:二维数组,每项代表一个菜单分组
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function AdminPowerMenu()
|
|
|
|
|
|
{
|
|
|
|
|
|
return [
|
|
|
|
|
|
// 座位模板
|
|
|
|
|
|
[
|
|
|
|
|
|
'name' => '座位模板',
|
|
|
|
|
|
'control' => 'seat_template',
|
|
|
|
|
|
'action' => 'list',
|
|
|
|
|
|
'item' => [
|
|
|
|
|
|
['name' => '座位模板', 'action' => 'list'],
|
|
|
|
|
|
['name' => '添加模板', 'action' => 'save'],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
// 电子票
|
|
|
|
|
|
[
|
|
|
|
|
|
'name' => '电子票',
|
|
|
|
|
|
'control' => 'ticket',
|
|
|
|
|
|
'action' => 'list',
|
|
|
|
|
|
'item' => [
|
|
|
|
|
|
['name' => '电子票列表', 'action' => 'list'],
|
|
|
|
|
|
['name' => '票详情', 'action' => 'detail'],
|
|
|
|
|
|
['name' => '手动核销', 'action' => 'verify'],
|
|
|
|
|
|
['name' => '导出票', 'action' => 'export'],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
// 核销员
|
|
|
|
|
|
[
|
|
|
|
|
|
'name' => '核销员',
|
|
|
|
|
|
'control' => 'verifier',
|
|
|
|
|
|
'action' => 'list',
|
|
|
|
|
|
'item' => [
|
|
|
|
|
|
['name' => '核销员列表', 'action' => 'list'],
|
|
|
|
|
|
['name' => '添加核销员', 'action' => 'save'],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
// 核销记录
|
|
|
|
|
|
[
|
|
|
|
|
|
'name' => '核销记录',
|
|
|
|
|
|
'control' => 'verification',
|
|
|
|
|
|
'action' => 'list',
|
|
|
|
|
|
'item' => [
|
|
|
|
|
|
['name' => '核销记录', 'action' => 'list'],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|