1067 lines
38 KiB
PHP
1067 lines
38 KiB
PHP
<?php
|
||
/**
|
||
* VR票务插件 - 后台管理主控制器(admin/Admin.php 模式)
|
||
*
|
||
* 路由机制(Plugins/Index → PluginsService::PluginsControlCall):
|
||
* sidebar URL: /plugins/vr_ticket/admin/seatTemplateList
|
||
* → pluginsname=vr_ticket, pluginscontrol=admin, pluginsaction=seatTemplateList
|
||
* → class = \app\plugins\vr_ticket\admin\Admin (ucfirst('admin') = 'Admin')
|
||
* → method = ucfirst('seatTemplateList') = 'SeatTemplateList'
|
||
* → app/plugins/vr_ticket/admin/Admin.php::SeatTemplateList() ✓
|
||
*
|
||
* ThinkPHP PSR-4 autoload: app\ → app/
|
||
* 所以 \app\plugins\vr_ticket\admin\Admin
|
||
* → app/plugins/vr_ticket/admin/Admin.php ✓
|
||
*
|
||
* 旧结构 admin/controller/SeatTemplate.php (namespace 含 controller 子目录)
|
||
* 会产生类路径 \app\plugins\vr_ticket\admin\SeatTemplate(无 controller 子目录)
|
||
* → app/plugins/vr_ticket/admin/SeatTemplate.php ✗ (不存在)
|
||
* 这就是路由失败的根因!
|
||
*
|
||
* @package vr_ticket\admin
|
||
*/
|
||
|
||
namespace app\plugins\vr_ticket\admin;
|
||
|
||
define('START_TIME', microtime(true));
|
||
|
||
use app\admin\controller\Common;
|
||
|
||
/**
|
||
* 所有后台控制器方法都在此类中实现
|
||
* 直接继承 ShopXO Common 控制器以获得 IsLogin + IsPower + ViewInit
|
||
*/
|
||
class Admin extends Common
|
||
{
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
}
|
||
|
||
/**
|
||
* 初始化方法 - 自愈性安装检查
|
||
*/
|
||
protected function initialize()
|
||
{
|
||
parent::initialize();
|
||
|
||
// 增加缓存锁,避免每次请求都进行数据库结构自检(这是导致加载缓慢的潜在原因)
|
||
$cache_key = 'vr_ticket_table_checked_v2';
|
||
if (\think\facade\Cache::get($cache_key) !== 1) {
|
||
try {
|
||
$this->checkAndInstallTables();
|
||
\think\facade\Cache::set($cache_key, 1, 3600); // 1小时检查一次
|
||
} catch (\Exception $e) {
|
||
// 静默失败
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 自动建表逻辑
|
||
* 如果发现核心表不存在,则从 install.sql 读取 SQL 并执行
|
||
*/
|
||
private function checkAndInstallTables()
|
||
{
|
||
$prefix = \think\facade\Config::get('database.connections.mysql.prefix', 'vrt_');
|
||
$tableName = $prefix . 'vr_seat_templates';
|
||
|
||
// 检查表是否存在
|
||
$res = \think\facade\Db::query("SHOW TABLES LIKE '{$tableName}'");
|
||
if (empty($res)) {
|
||
$sqlFile = dirname(__DIR__) . '/install.sql';
|
||
if (file_exists($sqlFile)) {
|
||
$sqlContent = file_get_contents($sqlFile);
|
||
// 替换前缀
|
||
$sqlContent = str_replace('{{prefix}}', $prefix, $sqlContent);
|
||
// 拆分 SQL 语句执行
|
||
$sqls = explode(';', $sqlContent);
|
||
foreach ($sqls as $sql) {
|
||
$sql = trim($sql);
|
||
if (!empty($sql)) {
|
||
try {
|
||
\think\facade\Db::execute($sql);
|
||
} catch (\Exception $e) {
|
||
// 记录日志或忽略错误
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 兼容性修补:如果已存在表且存在 uk_category_id 唯一索引,则将其删除
|
||
// 场馆现在使用自由配置,不再强制要求绑定唯一的产品分类
|
||
try {
|
||
$indexCheck = \think\facade\Db::query("SHOW INDEX FROM `{$tableName}` WHERE Key_name = 'uk_category_id'");
|
||
if (!empty($indexCheck)) {
|
||
try {
|
||
\think\facade\Db::execute("ALTER TABLE `{$tableName}` DROP INDEX `uk_category_id`");
|
||
} catch (\Exception $e) {
|
||
// 如果已经手动删除或报错则忽略
|
||
}
|
||
}
|
||
} catch (\Exception $e) {
|
||
// 忽略由于键不存在导致的异常
|
||
}
|
||
}
|
||
public function index()
|
||
{
|
||
return $this->VenueList();
|
||
}
|
||
|
||
|
||
// ============================================================
|
||
// 座位模板(SeatTemplate)
|
||
// 视图: admin/view/seat_template/{action}.html
|
||
// ============================================================
|
||
|
||
/**
|
||
* 座位模板列表
|
||
* URL: /plugins/vr_ticket/admin/seatTemplateList
|
||
* → PluginsService ucfirst('admin')=Admin + ucfirst('seatTemplateList')=SeatTemplateList
|
||
*/
|
||
public function SeatTemplateList()
|
||
{
|
||
$where = [];
|
||
|
||
$name = input('name', '', null);
|
||
if ($name !== '') {
|
||
$where[] = ['name', 'like', "%{$name}%"];
|
||
}
|
||
|
||
$status = input('status', '', null);
|
||
if ($status !== '' && $status !== null) {
|
||
$where[] = ['status', '=', intval($status)];
|
||
}
|
||
|
||
$list = \think\facade\Db::name('vr_seat_templates')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->paginate(20);
|
||
$list_data = $list->toArray();
|
||
|
||
// 关联分类名
|
||
$category_ids = array_filter(array_column($list_data['data'], 'category_id'));
|
||
if (!empty($category_ids)) {
|
||
$categories = \think\facade\Db::name('GoodsCategory')
|
||
->where('id', 'in', $category_ids)
|
||
->column('name', 'id');
|
||
foreach ($list_data['data'] as &$item) {
|
||
$item['category_name'] = $categories[$item['category_id']] ?? '未知分类';
|
||
$item['seat_count'] = $this->countSeats($item['seat_map']);
|
||
}
|
||
unset($item);
|
||
}
|
||
|
||
// Leading / = ThinkPHP absolute path resolved from app/admin/view/default/
|
||
// Files are at: app/admin/view/default/plugins/view/vr_ticket/admin/view/seat_template/list.html
|
||
return MyView('../../../plugins/vr_ticket/admin/view/seat_template/list', [
|
||
'list' => $list_data['data'],
|
||
'page' => $list->render() ?: '',
|
||
'count' => $list_data['total'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 添加/编辑座位模板
|
||
*/
|
||
public function SeatTemplateSave()
|
||
{
|
||
$id = input('id', 0, 'intval');
|
||
|
||
if ((request()->isAjax() && request()->isPost())) {
|
||
$data = [
|
||
'name' => input('name', '', null, 'trim'),
|
||
'category_id' => input('category_id', 0, 'intval'),
|
||
'seat_map' => input('seat_map', '', null, 'trim'),
|
||
'spec_base_id_map' => input('spec_base_id_map', '', null, 'trim'),
|
||
'status' => input('status', 1, 'intval'),
|
||
'upd_time' => time(),
|
||
];
|
||
|
||
if (empty($data['name'])) {
|
||
return DataReturn('模板名称不能为空', -1);
|
||
}
|
||
if (empty($data['category_id'])) {
|
||
return DataReturn('请选择绑定的分类', -1);
|
||
}
|
||
|
||
// 验证 seat_map 为合法 JSON
|
||
$seat_map = json_decode($data['seat_map'], true);
|
||
if (empty($seat_map) && $data['seat_map'] !== '[]' && $data['seat_map'] !== '{}') {
|
||
return DataReturn('座位地图JSON格式错误', -1);
|
||
}
|
||
|
||
if ($id > 0) {
|
||
\think\facade\Db::name('vr_seat_templates')->where('id', $id)->update($data);
|
||
return DataReturn('更新成功', 0);
|
||
} else {
|
||
$data['add_time'] = time();
|
||
$data['upd_time'] = time();
|
||
\think\facade\Db::name('vr_seat_templates')->insert($data);
|
||
return DataReturn('添加成功', 0);
|
||
}
|
||
}
|
||
|
||
// 编辑时加载数据
|
||
$info = [];
|
||
if ($id > 0) {
|
||
$info = \think\facade\Db::name('vr_seat_templates')->find($id);
|
||
}
|
||
|
||
// 加载分类列表(用于下拉选择)
|
||
$categories = \think\facade\Db::name('GoodsCategory')
|
||
->where('is_enable', 1)
|
||
->order('id', 'asc')
|
||
->select();
|
||
|
||
return MyView('../../../plugins/vr_ticket/admin/view/seat_template/save', [
|
||
'info' => $info,
|
||
'categories' => $categories,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 删除座位模板(软删除)
|
||
*/
|
||
public function SeatTemplateDelete()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$id = input('id', 0, 'intval');
|
||
if ($id <= 0) {
|
||
return DataReturn('参数错误', -1);
|
||
}
|
||
|
||
$hardDelete = input('hard_delete', 0, 'intval');
|
||
$template = \think\facade\Db::name('vr_seat_templates')->where('id', $id)->find();
|
||
|
||
if (empty($template)) {
|
||
return DataReturn('记录不存在', -1);
|
||
}
|
||
|
||
if ($hardDelete) {
|
||
// 检查是否有关联商品(ShopXO 使用 is_delete_time 做软删除判断)
|
||
$goods = \think\facade\Db::name('Goods')
|
||
->where('vr_goods_config', 'like', '%"template_id":' . $id . '%')
|
||
->where('is_delete_time', 0)
|
||
->find();
|
||
\think\facade\Db::name('vr_seat_templates')->where('id', $id)->delete();
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_DELETE_TEMPLATE,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE,
|
||
$id,
|
||
['name' => $template['name'], 'has_goods' => !empty($goods)],
|
||
"模板: {$template['name']}"
|
||
);
|
||
return DataReturn('删除成功', 0, ['has_goods' => !empty($goods)]);
|
||
}
|
||
|
||
// 软删除(禁用)
|
||
\think\facade\Db::name('vr_seat_templates')
|
||
->where('id', $id)
|
||
->update(['status' => 0, 'upd_time' => time()]);
|
||
|
||
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_DISABLE_TEMPLATE,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE,
|
||
$id,
|
||
['before_status' => $template['status'] ?? 1],
|
||
$template ? "模板: {$template['name']}" : "ID:{$id}"
|
||
);
|
||
|
||
return DataReturn('已禁用', 0);
|
||
}
|
||
|
||
public function SeatTemplateEnable()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$id = input('id', 0, 'intval');
|
||
if ($id <= 0) {
|
||
return DataReturn('参数错误', -1);
|
||
}
|
||
|
||
\think\facade\Db::name('vr_seat_templates')
|
||
->where('id', $id)
|
||
->update(['status' => 1, 'upd_time' => time()]);
|
||
|
||
\think\facade\Db::name('vr_seat_templates')
|
||
->where('id', $id)
|
||
->update(['status' => 1, 'upd_time' => time()]);
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_ENABLE_TEMPLATE,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE,
|
||
$id,
|
||
['after_status' => 1],
|
||
"模板ID:{$id}"
|
||
);
|
||
return DataReturn('已启用', 0);
|
||
}
|
||
|
||
// ============================================================
|
||
// 电子票(Ticket)
|
||
// 视图: admin/view/ticket/{action}.html
|
||
// ============================================================
|
||
|
||
/**
|
||
* 电子票列表
|
||
*/
|
||
public function TicketList()
|
||
{
|
||
$where = [];
|
||
|
||
$keywords = input('keywords', '', null, 'trim');
|
||
if (!empty($keywords)) {
|
||
$where[] = ['order_no|ticket_code|real_name|phone', 'like', "%{$keywords}%"];
|
||
}
|
||
|
||
$verify_status = input('verify_status', '', null);
|
||
if ($verify_status !== '' && $verify_status !== null) {
|
||
$where[] = ['verify_status', '=', intval($verify_status)];
|
||
}
|
||
|
||
$goods_id = input('goods_id', 0, 'intval');
|
||
if ($goods_id > 0) {
|
||
$where[] = ['goods_id', '=', $goods_id];
|
||
}
|
||
|
||
$list = \think\facade\Db::name('vr_tickets')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->paginate(20);
|
||
$list_data = $list->toArray();
|
||
|
||
// 补充商品名称
|
||
$goods_ids = array_filter(array_column($list_data['data'], 'goods_id'));
|
||
if (!empty($goods_ids)) {
|
||
$goods_map = \think\facade\Db::name('Goods')
|
||
->where('id', 'in', $goods_ids)
|
||
->column('title', 'id');
|
||
foreach ($list_data['data'] as &$item) {
|
||
$item['goods_title'] = $goods_map[$item['goods_id']] ?? '已删除商品';
|
||
$item['qr_code_url'] = \app\plugins\vr_ticket\service\TicketService::getQrCodeUrl($item['ticket_code']);
|
||
}
|
||
unset($item);
|
||
}
|
||
|
||
$status_map = [
|
||
0 => ['text' => '未核销', 'color' => 'blue'],
|
||
1 => ['text' => '已核销', 'color' => 'green'],
|
||
2 => ['text' => '已退款', 'color' => 'red'],
|
||
];
|
||
|
||
return MyView('../../../plugins/vr_ticket/admin/view/ticket/list', [
|
||
'list' => $list_data['data'],
|
||
'page' => $list->render() ?: '',
|
||
'count' => $list_data['total'],
|
||
'status_map' => $status_map,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 票详情
|
||
*/
|
||
public function TicketDetail()
|
||
{
|
||
$id = input('id', 0, 'intval');
|
||
if ($id <= 0) {
|
||
return DataReturn('参数错误', -1);
|
||
}
|
||
|
||
$ticket = \think\facade\Db::name('vr_tickets')->find($id);
|
||
if (empty($ticket)) {
|
||
return DataReturn('票不存在', -1);
|
||
}
|
||
|
||
$goods = \think\facade\Db::name('Goods')->find($ticket['goods_id']);
|
||
|
||
$verifier = [];
|
||
if ($ticket['verifier_id'] > 0) {
|
||
$verifier = \think\facade\Db::name('vr_verifiers')->find($ticket['verifier_id']);
|
||
}
|
||
|
||
$ticket['qr_code_url'] = \app\plugins\vr_ticket\service\TicketService::getQrCodeUrl($ticket['ticket_code']);
|
||
|
||
$verifiers = \think\facade\Db::name('vr_verifiers')
|
||
->where('status', 1)
|
||
->order('id', 'asc')
|
||
->select();
|
||
|
||
return MyView('../../../plugins/vr_ticket/admin/view/ticket/detail', [
|
||
'ticket' => $ticket,
|
||
'goods' => $goods,
|
||
'verifier' => $verifier,
|
||
'verifiers' => $verifiers,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 手动核销票(JSON API)
|
||
*/
|
||
public function TicketVerify()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$ticket_code = input('ticket_code', '', null, 'trim');
|
||
$verifier_id = input('verifier_id', 0, 'intval');
|
||
|
||
if (empty($ticket_code)) {
|
||
return DataReturn('票码不能为空', -1);
|
||
}
|
||
if ($verifier_id <= 0) {
|
||
return DataReturn('请选择核销员', -1);
|
||
}
|
||
|
||
$result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id);
|
||
return DataReturn($result['msg'], $result['code'], $result['data'] ?? []);
|
||
}
|
||
|
||
/**
|
||
* 导出票列表(CSV)
|
||
*/
|
||
public function TicketExport()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$where = [];
|
||
$goods_id = input('goods_id', 0, 'intval');
|
||
if ($goods_id > 0) {
|
||
$where[] = ['goods_id', '=', $goods_id];
|
||
}
|
||
|
||
$header = ['ID', '订单号', '票码', '观演人', '手机', '座位', '核销状态', '发放时间'];
|
||
$rows = \think\facade\Db::name('vr_tickets')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->cursor();
|
||
|
||
$data = [];
|
||
foreach ($rows as $item) {
|
||
$status_text = $item['verify_status'] == 0 ? '未核销' : ($item['verify_status'] == 1 ? '已核销' : '已退款');
|
||
$data[] = [
|
||
$item['id'],
|
||
$item['order_no'],
|
||
$item['ticket_code'],
|
||
$item['real_name'],
|
||
$item['phone'],
|
||
$item['seat_info'],
|
||
$status_text,
|
||
date('Y-m-d H:i:s', $item['issued_at']),
|
||
];
|
||
}
|
||
|
||
\app\plugins\vr_ticket\service\AuditService::logExport($goods_id, ['verify_status' => null], count($data));
|
||
|
||
ExportCsv($header, $data, 'vr_tickets_' . date('Ymd'));
|
||
return;
|
||
}
|
||
|
||
// ============================================================
|
||
// 核销员(Verifier)
|
||
// 视图: admin/view/verifier/{action}.html
|
||
// ============================================================
|
||
|
||
/**
|
||
* 核销员列表
|
||
*/
|
||
public function VerifierList()
|
||
{
|
||
$where = [];
|
||
|
||
$keywords = input('keywords', '', null, 'trim');
|
||
if (!empty($keywords)) {
|
||
$where[] = ['name|user_id', 'like', "%{$keywords}%"];
|
||
}
|
||
|
||
$status = input('status', '', null);
|
||
if ($status !== '' && $status !== null) {
|
||
$where[] = ['status', '=', intval($status)];
|
||
}
|
||
|
||
$list = \think\facade\Db::name('vr_verifiers')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->paginate(20);
|
||
$list_data = $list->toArray();
|
||
|
||
// 关联 ShopXO 用户信息
|
||
$user_ids = array_filter(array_column($list_data['data'], 'user_id'));
|
||
if (!empty($user_ids)) {
|
||
$users_raw = \think\facade\Db::name('User')
|
||
->where('id', 'in', $user_ids)
|
||
->select();
|
||
$users = [];
|
||
foreach ($users_raw as $u) {
|
||
$users[$u['id']] = ($u['nickname'] ?: '') . '/' . ($u['username'] ?: '');
|
||
}
|
||
foreach ($list_data['data'] as &$item) {
|
||
$item['user_name'] = $users[$item['user_id']] ?? '已删除用户';
|
||
}
|
||
unset($item);
|
||
}
|
||
|
||
return MyView('../../../plugins/vr_ticket/admin/view/verifier/list', [
|
||
'list' => $list_data['data'],
|
||
'page' => $list->render() ?: '',
|
||
'count' => $list_data['total'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 添加/编辑核销员
|
||
*/
|
||
public function VerifierSave()
|
||
{
|
||
$id = input('id', 0, 'intval');
|
||
|
||
if ((request()->isAjax() && request()->isPost())) {
|
||
$user_id = input('user_id', 0, 'intval');
|
||
$name = input('name', '', null, 'trim');
|
||
$status = input('status', 1, 'intval');
|
||
|
||
if ($user_id <= 0) {
|
||
return DataReturn('请选择关联用户', -1);
|
||
}
|
||
if (empty($name)) {
|
||
return DataReturn('核销员名称不能为空', -1);
|
||
}
|
||
|
||
$exist = \think\facade\Db::name('vr_verifiers')
|
||
->where('user_id', $user_id)
|
||
->where('id', '<>', $id)
|
||
->find();
|
||
if ($exist) {
|
||
return DataReturn('该用户已是核销员', -1);
|
||
}
|
||
|
||
if ($id > 0) {
|
||
\think\facade\Db::name('vr_verifiers')
|
||
->where('id', $id)
|
||
->update(['name' => $name, 'status' => $status]);
|
||
return DataReturn('更新成功', 0);
|
||
} else {
|
||
\think\facade\Db::name('vr_verifiers')->insert([
|
||
'user_id' => $user_id,
|
||
'name' => $name,
|
||
'status' => $status,
|
||
'created_at' => time(),
|
||
]);
|
||
return DataReturn('添加成功', 0);
|
||
}
|
||
}
|
||
|
||
$info = [];
|
||
if ($id > 0) {
|
||
$info = \think\facade\Db::name('vr_verifiers')->find($id);
|
||
}
|
||
|
||
$users = \think\facade\Db::name('User')
|
||
->where('status', '<>', 3) // 3 usually means deleted/disabled in some versions, but to be safe:
|
||
->field('id, nickname, username')
|
||
->order('id', 'desc')
|
||
->select();
|
||
|
||
return MyView('../../../plugins/vr_ticket/admin/view/verifier/save', [
|
||
'info' => $info,
|
||
'users' => $users,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 删除核销员(软删除:禁用)
|
||
*/
|
||
public function VerifierDelete()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$id = input('id', 0, 'intval');
|
||
if ($id <= 0) {
|
||
return DataReturn('参数错误', -1);
|
||
}
|
||
|
||
$verifier = \think\facade\Db::name('vr_verifiers')->where('id', $id)->find();
|
||
\think\facade\Db::name('vr_verifiers')
|
||
->where('id', $id)
|
||
->update(['status' => 0]);
|
||
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_DISABLE_VERIFIER,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_VERIFIER,
|
||
$id,
|
||
['before_status' => $verifier['status'] ?? 1],
|
||
$verifier ? "核销员: {$verifier['name']}" : "ID:{$id}"
|
||
);
|
||
|
||
return DataReturn('已禁用', 0);
|
||
}
|
||
|
||
// ============================================================
|
||
// 场馆配置(Venue)
|
||
// 视图: admin/view/venue/{action}.html
|
||
// 注意:admin/controller/Venue.php 的旧控制器仍在使用,
|
||
// 但新路由走 Admin.php 的 VenueList/VenueSave。
|
||
// ============================================================
|
||
|
||
/**
|
||
* 场馆列表
|
||
* URL: /plugins/vr_ticket/admin/venueList
|
||
*/
|
||
public function VenueList()
|
||
{
|
||
$where = [];
|
||
|
||
$name = input('name', '', null);
|
||
if ($name !== '') {
|
||
$where[] = ['name', 'like', "%{$name}%"];
|
||
}
|
||
|
||
$status = input('status', '', null);
|
||
if ($status !== '' && $status !== null) {
|
||
$where[] = ['status', '=', intval($status)];
|
||
}
|
||
|
||
$list = \think\facade\Db::name('vr_seat_templates')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->paginate(20);
|
||
$list_data = $list->toArray();
|
||
|
||
// 解析 venue.name 和座位数(v3.0 格式:seat_map.venue.name)
|
||
foreach ($list_data['data'] as &$item) {
|
||
$seatMap = json_decode($item['seat_map'] ?? '{}', true);
|
||
$item['venue_name'] = $seatMap['venue']['name'] ?? $item['name'];
|
||
$item['venue_address'] = $seatMap['venue']['address'] ?? '';
|
||
$item['rooms'] = $seatMap['rooms'] ?? [];
|
||
$zoneCount = 0;
|
||
if (!empty($item['rooms'])) {
|
||
foreach ($item['rooms'] as $rm) {
|
||
$zoneCount += count($rm['sections'] ?? []);
|
||
}
|
||
} else {
|
||
$zoneCount = !empty($seatMap['sections']) ? count($seatMap['sections']) : 0;
|
||
}
|
||
$item['zone_count'] = $zoneCount;
|
||
$item['seat_count'] = $this->countSeatsV2($seatMap);
|
||
}
|
||
unset($item);
|
||
|
||
return MyView('../../../plugins/vr_ticket/view/venue/list', [
|
||
'list' => $list_data['data'],
|
||
'page' => $list->render() ?: '',
|
||
'count' => $list_data['total'],
|
||
'data_req' => input(),
|
||
'debug_time' => microtime(true) - START_TIME,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 添加/编辑场馆
|
||
* URL: /plugins/vr_ticket/admin/venueSave
|
||
*/
|
||
public function VenueSave()
|
||
{
|
||
$id = input('id', 0, 'intval');
|
||
|
||
if ((request()->isAjax() && request()->isPost())) {
|
||
$data = [
|
||
'name' => input('name', '', null, 'trim'),
|
||
'category_id' => 0, // 分类绑定已弃用,强置为 0
|
||
'status' => input('status', 1, 'intval'),
|
||
'upd_time' => time(),
|
||
];
|
||
|
||
if (empty($data['name'])) {
|
||
return DataReturn('场馆名称不能为空', -1);
|
||
}
|
||
|
||
// 使用 request()->post() 获取原始字符串,并兼容 Base64 编码绕过净化
|
||
$seat_map_raw = request()->post('seat_map_raw', '{}');
|
||
$decoded_json = $seat_map_raw;
|
||
if(!empty($seat_map_raw) && $seat_map_raw !== '{}') {
|
||
// 判断是否是 Base64 字符串 (如果是以 ey 开头通常是 JSON 的 Base64)
|
||
if(preg_match('/^[a-zA-Z0-9\/\+=]+$/', $seat_map_raw) && strlen($seat_map_raw) % 4 == 0) {
|
||
$decoded_json = base64_decode($seat_map_raw);
|
||
} else {
|
||
// 如果不是 Base64 则按原样处理并进行 htmlspecialchars_decode
|
||
$decoded_json = htmlspecialchars_decode($seat_map_raw);
|
||
}
|
||
}
|
||
$seat_map = json_decode($decoded_json, true);
|
||
|
||
if (empty($seat_map) || !is_array($seat_map)) {
|
||
return DataReturn('场馆配置数据无效或解析失败', -1);
|
||
}
|
||
|
||
// 基本验证
|
||
if (empty($seat_map['venue']['name'])) {
|
||
return DataReturn('场馆详情名称不能为空', -1);
|
||
}
|
||
|
||
$rooms = $seat_map['rooms'] ?? [];
|
||
if (!is_array($rooms) || empty($rooms)) {
|
||
return DataReturn('请至少添加一个放映室/展厅', -1);
|
||
}
|
||
|
||
foreach ($rooms as &$room) {
|
||
if (empty($room['name'])) {
|
||
return DataReturn('放映室名称不能为空', -1);
|
||
}
|
||
|
||
// 生成 room.id(兜底:保证每个房间有唯一 id,支持前端按 id 引用)
|
||
// 使用 random_int() (CSPRNG),UUID v4 格式(版本=4,变体=10xx)
|
||
if (empty($room['id'])) {
|
||
$room['id'] = sprintf('%08x-%04x-%04x-%04x-%04x%08x',
|
||
time(),
|
||
random_int(0, 0xffff),
|
||
random_int(0, 0xffff),
|
||
(random_int(0, 0x3fff) & 0x0fff) | 0x4000, // 版本4 + 变体10xx
|
||
random_int(0, 0xffff),
|
||
random_int(0, 0xffffffff));
|
||
}
|
||
|
||
// --- 自动补全 seats 字典,解决 'A' 未定义报错 ---
|
||
$room['seats'] = [];
|
||
if (!empty($room['sections']) && is_array($room['sections'])) {
|
||
foreach ($room['sections'] as $sec) {
|
||
if (isset($sec['char']) && $sec['char'] !== '') {
|
||
$room['seats'][$sec['char']] = $sec;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理 map 数组,过滤空行并修剪
|
||
$map = is_array($room['map']) ? $room['map'] : [];
|
||
$room['map'] = array_values(array_filter(array_map('trim', $map), function($v) {
|
||
return $v !== '';
|
||
}));
|
||
|
||
if (empty($room['map'])) {
|
||
return DataReturn("放映室 {$room['name']} 座位排布不能为空", -1);
|
||
}
|
||
foreach ($room['map'] as $rowStr) {
|
||
// 此时 $rowStr 已经 trim 过且非空
|
||
foreach (str_split($rowStr) as $char) {
|
||
if ($char !== '_' && $char !== '-' && !isset($room['seats'][$char])) {
|
||
return DataReturn("放映室 {$room['name']} 中座位字符 '{$char}' 未在分区中定义", -1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存最终 JSON,注意避免反斜杠转义
|
||
// 注意:此时 $rooms 已被引用并修改了 map 数组(过滤了空行)
|
||
$seat_map['rooms'] = $rooms;
|
||
$data['seat_map'] = json_encode($seat_map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
|
||
if ($id > 0) {
|
||
\think\facade\Db::name('vr_seat_templates')->where('id', $id)->update($data);
|
||
return DataReturn('更新成功', 0);
|
||
} else {
|
||
$data['add_time'] = time();
|
||
$data['upd_time'] = time();
|
||
$data['spec_base_id_map'] = '';
|
||
\think\facade\Db::name('vr_seat_templates')->insert($data);
|
||
return DataReturn('添加成功', 0);
|
||
}
|
||
}
|
||
|
||
$info = [];
|
||
if ($id > 0) {
|
||
$row = \think\facade\Db::name('vr_seat_templates')->find($id);
|
||
if (!empty($row)) {
|
||
$seatMap = json_decode($row['seat_map'] ?? '{}', true);
|
||
$row['venue_name'] = $seatMap['venue']['name'] ?? '';
|
||
$row['venue_address'] = $seatMap['venue']['address'] ?? '';
|
||
$row['venue_image'] = $seatMap['venue']['image'] ?? '';
|
||
$row['zones_json'] = json_encode($seatMap['sections'] ?? [], JSON_UNESCAPED_UNICODE);
|
||
$row['venue_json'] = json_encode([
|
||
'name' => $seatMap['venue']['name'] ?? '',
|
||
'address' => $seatMap['venue']['address'] ?? '',
|
||
'image' => $seatMap['venue']['image'] ?? '',
|
||
], JSON_UNESCAPED_UNICODE);
|
||
$row['map_json'] = json_encode($seatMap['map'] ?? [], JSON_UNESCAPED_UNICODE);
|
||
$info = $row;
|
||
}
|
||
}
|
||
|
||
$categories = \think\facade\Db::name('GoodsCategory')
|
||
->where('is_enable', 1)
|
||
->order('id', 'asc')
|
||
->select();
|
||
|
||
// 加载插件配置,用于获取高德 API Key 等
|
||
$config_ret = \app\service\PluginsService::PluginsData('vr_ticket');
|
||
$config = $config_ret['data'] ?? [];
|
||
|
||
return MyView('../../../plugins/vr_ticket/view/venue/save', [
|
||
'info' => $info,
|
||
'categories' => $categories,
|
||
'vr_config' => $config,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 插件设置视图
|
||
*/
|
||
public function Setup()
|
||
{
|
||
$data_ret = \app\service\PluginsService::PluginsData('vr_ticket');
|
||
$data = $data_ret['data'] ?? [];
|
||
return MyView('../../../plugins/vr_ticket/view/admin/setup', [
|
||
'data' => $data,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 插件设置保存
|
||
*/
|
||
public function SetupSave()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$data = [
|
||
'amap_api_key' => input('amap_api_key', '', null, 'trim'),
|
||
'other_config' => input('other_config', '', null, 'trim'),
|
||
];
|
||
|
||
$params = [
|
||
'plugins' => 'vr_ticket',
|
||
'data' => $data,
|
||
];
|
||
$ret = \app\service\PluginsService::PluginsDataSave($params);
|
||
if ($ret['code'] == 0) {
|
||
return DataReturn('保存成功', 0);
|
||
} else {
|
||
return DataReturn($ret['msg'], -1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除场馆(软删除/硬删除)
|
||
* @param hard_delete: 0=软删除(禁用), 1=硬删除 (兼容旧的自定义JS调用)
|
||
* @param value: 'hard'=硬删除 (submit-ajax新方式)
|
||
*/
|
||
public function VenueDelete()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$id = input('id', 0, 'intval');
|
||
if ($id <= 0) {
|
||
return DataReturn('参数错误', -1);
|
||
}
|
||
|
||
// 兼容旧方式: hard_delete=1 为硬删除
|
||
$hardDelete = input('hard_delete', 0, 'intval');
|
||
// 新方式: value='hard' 为硬删除
|
||
$value = input('value', '', null, 'trim');
|
||
if ($value === 'hard') {
|
||
$hardDelete = 1;
|
||
}
|
||
|
||
$template = \think\facade\Db::name('vr_seat_templates')->where('id', $id)->find();
|
||
if (empty($template)) {
|
||
return DataReturn('记录不存在', -1);
|
||
}
|
||
|
||
if ($hardDelete) {
|
||
// 检查是否有关联商品(使用 is_delete_time 而不是 is_delete)
|
||
$goods = \think\facade\Db::name('Goods')
|
||
->where('vr_goods_config', 'like', '%"template_id":' . $id . '%')
|
||
->where('is_delete_time', 0)
|
||
->find();
|
||
\think\facade\Db::name('vr_seat_templates')->where('id', $id)->delete();
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_DELETE_TEMPLATE,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE,
|
||
$id,
|
||
['name' => $template['name'], 'has_goods' => !empty($goods)],
|
||
"场馆: {$template['name']}"
|
||
);
|
||
return DataReturn('删除成功', 0, ['has_goods' => !empty($goods)]);
|
||
}
|
||
|
||
|
||
// 软删除(禁用)
|
||
\think\facade\Db::name('vr_seat_templates')
|
||
->where('id', $id)
|
||
->update(['status' => 0, 'upd_time' => time()]);
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_DISABLE_TEMPLATE,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE,
|
||
$id,
|
||
['before_status' => $template['status'] ?? 1],
|
||
$template ? "场馆: {$template['name']}" : "ID:{$id}"
|
||
);
|
||
return DataReturn('已禁用', 0);
|
||
}
|
||
|
||
public function VenueEnable()
|
||
{
|
||
if (!(request()->isAjax() && request()->isPost())) {
|
||
return DataReturn('非法请求', -1);
|
||
}
|
||
|
||
$id = input('id', 0, 'intval');
|
||
if ($id <= 0) {
|
||
return DataReturn('参数错误', -1);
|
||
}
|
||
|
||
\think\facade\Db::name('vr_seat_templates')
|
||
->where('id', $id)
|
||
->update(['status' => 1, 'upd_time' => time()]);
|
||
\app\plugins\vr_ticket\service\AuditService::log(
|
||
\app\plugins\vr_ticket\service\AuditService::ACTION_ENABLE_TEMPLATE,
|
||
\app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE,
|
||
$id,
|
||
['after_status' => 1],
|
||
"场馆ID:{$id}"
|
||
);
|
||
return DataReturn('已启用', 0);
|
||
}
|
||
|
||
// ============================================================
|
||
// 核销记录(Verification)
|
||
// 视图: admin/view/verification/{action}.html
|
||
// ============================================================
|
||
|
||
/**
|
||
* 核销记录列表
|
||
*/
|
||
public function VerificationList()
|
||
{
|
||
$where = [];
|
||
|
||
$keywords = input('keywords', '', null, 'trim');
|
||
if (!empty($keywords)) {
|
||
$where[] = ['ticket_code|verifier_name', 'like', "%{$keywords}%"];
|
||
}
|
||
|
||
$verifier_id = input('verifier_id', 0, 'intval');
|
||
if ($verifier_id > 0) {
|
||
$where[] = ['verifier_id', '=', $verifier_id];
|
||
}
|
||
|
||
$start_date = input('start_date', '', null, 'trim');
|
||
$end_date = input('end_date', '', null, 'trim');
|
||
if (!empty($start_date)) {
|
||
$where[] = ['created_at', '>=', strtotime($start_date)];
|
||
}
|
||
if (!empty($end_date)) {
|
||
$where[] = ['created_at', '<=', strtotime($end_date . ' 23:59:59')];
|
||
}
|
||
|
||
$list = \think\facade\Db::name('vr_verifications')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->paginate(20);
|
||
$list_data = $list->toArray();
|
||
|
||
// 补充票信息
|
||
$ticket_ids = array_filter(array_column($list_data['data'], 'ticket_id'));
|
||
if (!empty($ticket_ids)) {
|
||
$tickets_raw = \think\facade\Db::name('vr_tickets')
|
||
->where('id', 'in', $ticket_ids)
|
||
->select();
|
||
$tickets = [];
|
||
foreach ($tickets_raw as $t) {
|
||
$tickets[$t['id']] = $t;
|
||
}
|
||
foreach ($list_data['data'] as &$item) {
|
||
$ticket = $tickets[$item['ticket_id']] ?? [];
|
||
$item['seat_info'] = $ticket['seat_info'] ?? '';
|
||
$item['real_name'] = $ticket['real_name'] ?? '';
|
||
$item['goods_id'] = $ticket['goods_id'] ?? 0;
|
||
}
|
||
unset($item);
|
||
}
|
||
|
||
// 商品名
|
||
$goods_ids = array_filter(array_unique(array_column($list_data['data'], 'goods_id')));
|
||
if (!empty($goods_ids)) {
|
||
$goods_map = \think\facade\Db::name('Goods')
|
||
->where('id', 'in', $goods_ids)
|
||
->column('title', 'id');
|
||
foreach ($list_data['data'] as &$item) {
|
||
$item['goods_title'] = $goods_map[$item['goods_id']] ?? '已删除';
|
||
}
|
||
unset($item);
|
||
}
|
||
|
||
// 核销员列表(用于筛选)
|
||
$verifiers = \think\facade\Db::name('vr_verifiers')
|
||
->where('status', 1)
|
||
->column('name', 'id');
|
||
|
||
return MyView('../../../plugins/vr_ticket/admin/view/verification/list', [
|
||
'list' => $list_data['data'],
|
||
'page' => $list->render() ?: '',
|
||
'count' => $list_data['total'],
|
||
'verifiers' => $verifiers,
|
||
]);
|
||
}
|
||
|
||
// ============================================================
|
||
// 辅助方法
|
||
// ============================================================
|
||
|
||
/**
|
||
* 统计座位数(v1 格式:直接传入 seat_map JSON 字符串)
|
||
*/
|
||
private function countSeats($seat_map_json)
|
||
{
|
||
if (empty($seat_map_json)) {
|
||
return 0;
|
||
}
|
||
$map = json_decode($seat_map_json, true);
|
||
if (empty($map['seats']) || empty($map['map'])) {
|
||
return 0;
|
||
}
|
||
$count = 0;
|
||
foreach ($map['map'] as $row) {
|
||
foreach (str_split($row) as $char) {
|
||
if ($char !== '_' && isset($map['seats'][$char])) {
|
||
$count++;
|
||
}
|
||
}
|
||
}
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 统计座位数(支持 v3 多房间格式 或 v2 格式)
|
||
*/
|
||
private function countSeatsV2($seatMap)
|
||
{
|
||
if (empty($seatMap['rooms']) || !is_array($seatMap['rooms'])) {
|
||
return 0;
|
||
}
|
||
$total = 0;
|
||
foreach ($seatMap['rooms'] as $rm) {
|
||
if (!empty($rm['map']) && is_array($rm['map'])) {
|
||
foreach ($rm['map'] as $row) {
|
||
if (is_string($row)) {
|
||
$total += strlen(str_replace(['_', '-'], '', $row));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return $total;
|
||
}
|
||
}
|