vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/admin/Admin.php

814 lines
27 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票务插件 - 后台管理主控制器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;
use app\admin\controller\Common;
/**
* 所有后台控制器方法都在此类中实现
* 直接继承 ShopXO Common 控制器以获得 IsLogin + IsPower + ViewInit
*/
class Admin extends Common
{
public function __construct()
{
parent::__construct();
}
// ============================================================
// 座位模板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 = \Db::name('plugins_vr_seat_templates')
->where($where)
->order('id', 'desc')
->paginate(20)
->toArray();
// 关联分类名
$category_ids = array_filter(array_column($list['data'], 'category_id'));
if (!empty($category_ids)) {
$categories = \Db::name('GoodsCategory')
->where('id', 'in', $category_ids)
->column('name', 'id');
foreach ($list['data'] as &$item) {
$item['category_name'] = $categories[$item['category_id']] ?? '未知分类';
$item['seat_count'] = $this->countSeats($item['seat_map']);
}
unset($item);
}
return view('seat_template/list', [
'list' => $list['data'],
'page' => $list['page'],
'count' => $list['total'],
]);
}
/**
* 添加/编辑座位模板
*/
public function SeatTemplateSave()
{
$id = input('id', 0, 'intval');
if (IS_AJAX_POST) {
$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) {
\Db::name('plugins_vr_seat_templates')->where('id', $id)->update($data);
return DataReturn('更新成功', 0);
} else {
$data['add_time'] = time();
$data['upd_time'] = time();
\Db::name('plugins_vr_seat_templates')->insert($data);
return DataReturn('添加成功', 0);
}
}
// 编辑时加载数据
$info = [];
if ($id > 0) {
$info = \Db::name('plugins_vr_seat_templates')->find($id);
}
// 加载分类列表(用于下拉选择)
$categories = \Db::name('GoodsCategory')
->where('is_delete', 0)
->order('id', 'asc')
->select();
return view('seat_template/save', [
'info' => $info,
'categories' => $categories,
]);
}
/**
* 删除座位模板(软删除)
*/
public function SeatTemplateDelete()
{
if (!IS_AJAX_POST) {
return DataReturn('非法请求', -1);
}
$id = input('id', 0, 'intval');
if ($id <= 0) {
return DataReturn('参数错误', -1);
}
$template = \Db::name('plugins_vr_seat_templates')->where('id', $id)->find();
\Db::name('plugins_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);
}
// ============================================================
// 电子票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 = \Db::name('plugins_vr_tickets')
->where($where)
->order('id', 'desc')
->paginate(20)
->toArray();
// 补充商品名称
$goods_ids = array_filter(array_column($list['data'], 'goods_id'));
if (!empty($goods_ids)) {
$goods_map = \Db::name('Goods')
->where('id', 'in', $goods_ids)
->column('title', 'id');
foreach ($list['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 view('ticket/list', [
'list' => $list['data'],
'page' => $list['page'],
'count' => $list['total'],
'status_map' => $status_map,
]);
}
/**
* 票详情
*/
public function TicketDetail()
{
$id = input('id', 0, 'intval');
if ($id <= 0) {
return DataReturn('参数错误', -1);
}
$ticket = \Db::name('plugins_vr_tickets')->find($id);
if (empty($ticket)) {
return DataReturn('票不存在', -1);
}
$goods = \Db::name('Goods')->find($ticket['goods_id']);
$verifier = [];
if ($ticket['verifier_id'] > 0) {
$verifier = \Db::name('plugins_vr_verifiers')->find($ticket['verifier_id']);
}
$ticket['qr_code_url'] = \app\plugins\vr_ticket\service\TicketService::getQrCodeUrl($ticket['ticket_code']);
$verifiers = \Db::name('plugins_vr_verifiers')
->where('status', 1)
->order('id', 'asc')
->select();
return view('ticket/detail', [
'ticket' => $ticket,
'goods' => $goods,
'verifier' => $verifier,
'verifiers' => $verifiers,
]);
}
/**
* 手动核销票JSON API
*/
public function TicketVerify()
{
if (!IS_AJAX_POST) {
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 (!IS_AJAX_POST) {
return DataReturn('非法请求', -1);
}
$where = [];
$goods_id = input('goods_id', 0, 'intval');
if ($goods_id > 0) {
$where[] = ['goods_id', '=', $goods_id];
}
$header = ['ID', '订单号', '票码', '观演人', '手机', '座位', '核销状态', '发放时间'];
$rows = \Db::name('plugins_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 = \Db::name('plugins_vr_verifiers')
->where($where)
->order('id', 'desc')
->paginate(20)
->toArray();
// 关联 ShopXO 用户信息
$user_ids = array_filter(array_column($list['data'], 'user_id'));
if (!empty($user_ids)) {
$users_raw = \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'] as &$item) {
$item['user_name'] = $users[$item['user_id']] ?? '已删除用户';
}
unset($item);
}
return view('verifier/list', [
'list' => $list['data'],
'page' => $list['page'],
'count' => $list['total'],
]);
}
/**
* 添加/编辑核销员
*/
public function VerifierSave()
{
$id = input('id', 0, 'intval');
if (IS_AJAX_POST) {
$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 = \Db::name('plugins_vr_verifiers')
->where('user_id', $user_id)
->where('id', '<>', $id)
->find();
if ($exist) {
return DataReturn('该用户已是核销员', -1);
}
if ($id > 0) {
\Db::name('plugins_vr_verifiers')
->where('id', $id)
->update(['name' => $name, 'status' => $status]);
return DataReturn('更新成功', 0);
} else {
\Db::name('plugins_vr_verifiers')->insert([
'user_id' => $user_id,
'name' => $name,
'status' => $status,
'created_at' => time(),
]);
return DataReturn('添加成功', 0);
}
}
$info = [];
if ($id > 0) {
$info = \Db::name('plugins_vr_verifiers')->find($id);
}
$users = \Db::name('User')
->where('is_delete', 0)
->field('id, nickname, username')
->order('id', 'desc')
->select();
return view('verifier/save', [
'info' => $info,
'users' => $users,
]);
}
/**
* 删除核销员(软删除:禁用)
*/
public function VerifierDelete()
{
if (!IS_AJAX_POST) {
return DataReturn('非法请求', -1);
}
$id = input('id', 0, 'intval');
if ($id <= 0) {
return DataReturn('参数错误', -1);
}
$verifier = \Db::name('plugins_vr_verifiers')->where('id', $id)->find();
\Db::name('plugins_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 = \Db::name('plugins_vr_seat_templates')
->where($where)
->order('id', 'desc')
->paginate(20)
->toArray();
// 解析 venue.name 和座位数v3.0 格式seat_map.venue.name
foreach ($list['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['zone_count'] = !empty($seatMap['sections']) ? count($seatMap['sections']) : 0;
$item['seat_count'] = $this->countSeatsV2($seatMap);
}
unset($item);
return view('venue/list', [
'list' => $list['data'],
'page' => $list['page'],
'count' => $list['total'],
]);
}
/**
* 添加/编辑场馆
* URL: /plugins/vr_ticket/admin/venueSave
*/
public function VenueSave()
{
$id = input('id', 0, 'intval');
if (IS_AJAX_POST) {
$data = [
'name' => input('name', '', null, 'trim'),
'category_id' => input('category_id', 0, 'intval'),
'status' => input('status', 1, 'intval'),
'upd_time' => time(),
];
if (empty($data['name'])) {
return DataReturn('场馆名称不能为空', -1);
}
// v3.0 seat_map JSON 构建
$venue = [
'name' => input('venue_name', '', null, 'trim'),
'address' => input('venue_address', '', null, 'trim'),
'image' => input('venue_image', '', null, 'trim'),
];
if (empty($venue['name'])) {
return DataReturn('场馆名称不能为空', -1);
}
$zones_raw = input('zones', '[]', null, 'trim');
$zones = json_decode($zones_raw, true);
if (!is_array($zones) || empty($zones)) {
return DataReturn('请至少添加一个分区', -1);
}
$map_raw = input('seat_map_rows', '[]', null, 'trim');
$map = json_decode($map_raw, true);
if (!is_array($map) || empty($map)) {
return DataReturn('座位排布不能为空', -1);
}
$sections = [];
$seats = [];
$row_labels = [];
foreach ($zones as $zone) {
$char = strtoupper($zone['char'] ?? '');
if (empty($char)) {
return DataReturn('分区字符不能为空', -1);
}
$sections[] = [
'char' => $char,
'name' => $zone['name'] ?? '',
'color' => $zone['color'] ?? '#cccccc',
];
$seats[$char] = [
'price' => intval($zone['price'] ?? 0),
'color' => $zone['color'] ?? '#cccccc',
'label' => $zone['name'] ?? '',
];
}
foreach ($map as $rowStr) {
$rowStr = trim($rowStr);
if (empty($rowStr)) {
return DataReturn('座位排布每行不能为空', -1);
}
foreach (str_split($rowStr) as $char) {
if ($char !== '_' && !isset($seats[$char])) {
return DataReturn("座位排布中字符 '{$char}' 未在分区中定义", -1);
}
}
$row_labels[] = $rowStr[0];
}
$data['seat_map'] = json_encode([
'venue' => $venue,
'map' => $map,
'seats' => $seats,
'sections' => $sections,
'row_labels' => array_values(array_unique(array_merge(
array_column($sections, 'char'),
$row_labels
))),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($id > 0) {
\Db::name('plugins_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'] = '';
\Db::name('plugins_vr_seat_templates')->insert($data);
return DataReturn('添加成功', 0);
}
}
$info = [];
if ($id > 0) {
$row = \Db::name('plugins_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 = \Db::name('GoodsCategory')
->where('is_delete', 0)
->order('id', 'asc')
->select();
return view('venue/save', [
'info' => $info,
'categories' => $categories,
]);
}
/**
* 删除场馆(软删除)
*/
public function VenueDelete()
{
if (!IS_AJAX_POST) {
return DataReturn('非法请求', -1);
}
$id = input('id', 0, 'intval');
if ($id <= 0) {
return DataReturn('参数错误', -1);
}
$template = \Db::name('plugins_vr_seat_templates')->where('id', $id)->find();
\Db::name('plugins_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);
}
// ============================================================
// 核销记录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 = \Db::name('plugins_vr_verifications')
->where($where)
->order('id', 'desc')
->paginate(20)
->toArray();
// 补充票信息
$ticket_ids = array_filter(array_column($list['data'], 'ticket_id'));
if (!empty($ticket_ids)) {
$tickets_raw = \Db::name('plugins_vr_tickets')
->where('id', 'in', $ticket_ids)
->select();
$tickets = [];
foreach ($tickets_raw as $t) {
$tickets[$t['id']] = $t;
}
foreach ($list['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'], 'goods_id')));
if (!empty($goods_ids)) {
$goods_map = \Db::name('Goods')
->where('id', 'in', $goods_ids)
->column('title', 'id');
foreach ($list['data'] as &$item) {
$item['goods_title'] = $goods_map[$item['goods_id']] ?? '已删除';
}
unset($item);
}
// 核销员列表(用于筛选)
$verifiers = \Db::name('plugins_vr_verifiers')
->where('status', 1)
->column('name', 'id');
return view('verification/list', [
'list' => $list['data'],
'page' => $list['page'],
'count' => $list['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;
}
/**
* 统计座位数v2 格式:直接传入已解码的 seat_map 数组)
*/
private function countSeatsV2(array $seat_map)
{
if (empty($seat_map['seats']) || empty($seat_map['map'])) {
return 0;
}
$count = 0;
foreach ($seat_map['map'] as $row) {
foreach (str_split($row) as $char) {
if ($char !== '_' && isset($seat_map['seats'][$char])) {
$count++;
}
}
}
return $count;
}
}