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

288 lines
8.3 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
/**
* 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'],
],
],
];
}
}