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

814 lines
27 KiB
PHP
Raw Normal View History

<?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;
}
}