vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/service/BaseService.php

288 lines
8.3 KiB
PHP
Raw Normal View History

<?php
/**
* VR票务插件 - 基础服务
*
* @package vr_ticket\service
*/
namespace app\plugins\vr_ticket\service;
class BaseService
{
/**
* 获取插件表前缀
*/
public static function table($name)
{
return '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', '');
if (empty($secret)) {
throw new \Exception('[vr_ticket] VR_TICKET_QR_SECRET 环境变量未配置QR加密密钥不能为空。请在.env中设置VR_TICKET_QR_SECRET=<随机64字符字符串>');
}
return $secret;
}
/**
* 判断商品是否为票务商品
*
* @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);
}
}
/**
* 初始化票务商品规格
*
* 修复商品 112 broken 状态:
* 1. 设置 is_exist_many_spec = 1(启用多规格模式)
* 2. 插入 $vr- 规格类型(幂等,多次执行不重复)
*
* @param int $goodsId 商品ID
* @return array ['code' => 0, 'msg' => '...', 'data' => [...]]
*/
public static function initGoodsSpecs(int $goodsId): array
{
$goodsId = intval($goodsId);
if ($goodsId <= 0) {
return ['code' => -1, 'msg' => '商品ID无效'];
}
// 1. 检查商品是否存在
$goods = \Db::name('Goods')->where('id', $goodsId)->find();
if (empty($goods)) {
return ['code' => -2, 'msg' => "商品 {$goodsId} 不存在"];
}
$now = time();
// 2. 启用多规格模式
\Db::name('Goods')->where('id', $goodsId)->update([
'is_exist_many_spec' => 1,
'upd_time' => $now,
]);
// 3. 定义 $vr- 规格类型name => JSON value
$specTypes = [
'$vr-场馆' => '[{"name":"' . ($goods['venue_data'] ?? '国家体育馆') . '","images":""}]',
'$vr-分区' => '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]',
'$vr-时段' => '[{"name":"待选场次","images":""}]',
'$vr-座位号' => '[{"name":"待选座位","images":""}]',
];
$insertedCount = 0;
foreach ($specTypes as $name => $value) {
// 幂等:检查是否已存在
$exists = \Db::name('GoodsSpecType')
->where('goods_id', $goodsId)
->where('name', $name)
->find();
if (empty($exists)) {
\Db::name('GoodsSpecType')->insert([
'goods_id' => $goodsId,
'name' => $name,
'value' => $value,
'add_time' => $now,
]);
$insertedCount++;
self::log('initGoodsSpecs: inserted spec_type', ['goods_id' => $goodsId, 'name' => $name]);
}
}
self::log('initGoodsSpecs: done', ['goods_id' => $goodsId, 'inserted' => $insertedCount]);
// 4. 返回当前所有 spec_type便于验证
$specTypes = \Db::name('GoodsSpecType')
->where('goods_id', $goodsId)
->order('id', 'asc')
->select()
->toArray();
return [
'code' => 0,
'msg' => "初始化完成,插入 {$insertedCount} 条规格类型",
'data' => [
'goods_id' => $goodsId,
'is_exist_many_spec' => 1,
'spec_types' => $specTypes,
],
];
}
/**
* 插件后台权限菜单
*
* 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'],
],
],
];
}
}