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) { $goods = \think\facade\Db::name('Goods') ->where('vr_goods_config', 'like', '%"template_id":' . $id . '%') ->where('is_delete', 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); } } /** * 删除场馆(软删除) */ public function VenueDelete() { 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) { $goods = \think\facade\Db::name('Goods') ->where('vr_goods_config', 'like', '%"template_id":' . $id . '%') ->where('is_delete', 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; } }