vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/api/Goods.php

455 lines
15 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票务插件 - C端商品API控制器
*
* 路由: /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=xxx
*
* @package vr_ticket\api
*/
namespace app\plugins\vr_ticket\api;
use app\plugins\vr_ticket\service\SeatMapService;
use app\plugins\vr_ticket\service\QueryManager;
use app\service\GoodsService;
/**
* C端商品 API
*/
class Goods
{
private static function success($data = [], string $msg = 'success')
{
return [
'code' => 0,
'msg' => $msg,
'data' => $data,
];
}
private static function error(string $msg = '请求失败', int $code = -1)
{
return [
'code' => $code,
'msg' => $msg,
'data' => [],
];
}
/**
* 获取热门推荐商品
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=recommend
*/
public function recommend()
{
try {
// 调用 ShopXO 商品服务获取热门商品
// VR票务插件的 category_id 需要在商品管理中设置
$params = [
'is_new' => 0,
'is_recommend' => 1,
'is_error' => 0,
'is_delete_time' => 0,
'start' => 0,
'num' => 10,
'order_by' => 'sales',
'sort' => 'desc',
];
$result = GoodsService::GoodsList($params);
$list = self::formatGoodsList($result);
return self::success([
'list' => $list,
'count' => count($list),
]);
} catch (\Exception $e) {
return self::error('获取推荐失败: ' . $e->getMessage());
}
}
/**
* 获取商品列表
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=lists
* @param int city_id 城市ID筛选
* @param int page 页码
* @param int size 每页数量
*/
public function lists()
{
try {
$page = input('page', 1, 'intval');
$size = input('size', 10, 'intval');
$cityId = input('city_id', 0, 'intval');
if (empty($cityId)) {
$cityId = input('cityid', 0, 'intval');
}
$start = ($page - 1) * $size;
$params = [
'is_new' => 0,
'is_error' => 0,
'is_delete_time' => 0,
'start' => $start,
'num' => $size,
'order_by' => 'add_time',
'sort' => 'desc',
];
// 城市筛选如果有设置produce_region
if (!empty($cityId)) {
$params['produce_region'] = $cityId;
}
$result = GoodsService::GoodsList($params);
$list = self::formatGoodsList($result);
return self::success([
'list' => $list,
'count' => count($list),
'page' => $page,
'size' => $size,
]);
} catch (\Exception $e) {
return self::error('获取列表失败: ' . $e->getMessage());
}
}
/**
* 获取周边商品
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=merchandise
*/
public function merchandise()
{
try {
$page = input('page', 1, 'intval');
$size = input('size', 20, 'intval');
$start = ($page - 1) * $size;
// 获取VR票务相关的周边商品非票务类型
$params = [
'is_new' => 0,
'is_error' => 0,
'is_delete_time' => 0,
'start' => $start,
'num' => $size,
'order_by' => 'sales',
'sort' => 'desc',
// 可以根据实际情况添加商品分类筛选
];
$result = GoodsService::GoodsList($params);
$list = self::formatGoodsList($result);
return self::success([
'list' => $list,
'count' => count($list),
]);
} catch (\Exception $e) {
return self::error('获取周边商品失败: ' . $e->getMessage());
}
}
/**
* 获取商品详情
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=detail&id=X
*/
public function detail()
{
$goodsId = input('id', 0, 'intval');
if ($goodsId <= 0) {
return self::error('参数错误商品ID无效');
}
try {
$goods = GoodsService::GoodsDetail($goodsId);
if (empty($goods)) {
return self::error('商品不存在', -404);
}
return self::success([
'goods' => self::formatGoodsDetail($goods),
]);
} catch (\Exception $e) {
return self::error('获取详情失败: ' . $e->getMessage());
}
}
/**
* 搜索商品
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=search&keyword=X
*/
public function search()
{
$keyword = input('keyword', '', 'trim');
$page = input('page', 1, 'intval');
$size = input('size', 10, 'intval');
if (empty($keyword)) {
return self::error('请输入搜索关键词');
}
try {
$start = ($page - 1) * $size;
$params = [
'is_new' => 0,
'is_error' => 0,
'is_delete_time' => 0,
'start' => $start,
'num' => $size,
'title_like' => $keyword,
'order_by' => 'sales',
'sort' => 'desc',
];
$result = GoodsService::GoodsList($params);
$list = self::formatGoodsList($result);
return self::success([
'list' => $list,
'count' => count($list),
'keyword' => $keyword,
]);
} catch (\Exception $e) {
return self::error('搜索失败: ' . $e->getMessage());
}
}
/**
* 获取座位图(含实时库存)
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=seatmap&goods_id=118
*
* @return array { code, msg, data: { seatSpecMap, goods_spec_data } }
*/
public function seatmap()
{
$goodsId = input('goods_id', 0, 'intval');
if ($goodsId <= 0) {
return self::error('参数错误goods_id 无效');
}
try {
$data = SeatMapService::GetSeatMap($goodsId);
return self::success($data);
} catch (\Exception $e) {
return self::error('获取座位图失败: ' . $e->getMessage());
}
}
/**
* 获取层级树 APITree API
*
* 返回tree + seat_templates_flat + flat_inventory
* 支持参数化 group_by 指定层级顺序
*
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree&goods_id=118&group_by=venue,session,room,section
*
* @return array { code, msg, data: { goods_id, group_by, tree, seat_templates_flat, flat_inventory, meta } }
*/
public function tree()
{
$goodsId = input('goods_id', 0, 'intval');
$groupByStr = input('group_by', 'venue,session,room,section', 'trim');
if ($goodsId <= 0) {
return self::error('goods_id 无效');
}
// 解析 group_by 参数
$groupBy = array_filter(array_map('trim', explode(',', $groupByStr)));
if (empty($groupBy)) {
return self::error('group_by 参数无效');
}
// 允许的维度
$allowedDims = ['venue', 'session', 'room', 'section'];
foreach ($groupBy as $dim) {
if (!in_array($dim, $allowedDims)) {
return self::error("不支持的维度: {$dim},允许: " . implode(',', $allowedDims));
}
}
try {
// 缓存检查
$cacheKey = 'vr_tree_v4_' . $goodsId . '_' . md5(implode(',', $groupBy));
$cached = \think\facade\Cache::get($cacheKey);
if ($cached !== null) {
$cached['meta']['cache_hit'] = true;
return self::success($cached);
}
// 读取数据源
$seatMapData = SeatMapService::GetSeatMap($goodsId);
$seatSpecMap = $seatMapData['seatSpecMap'] ?? [];
if (empty($seatSpecMap)) {
return self::success([
'goods_id' => $goodsId,
'group_by' => $groupBy,
'tree' => [],
'seat_templates' => new \stdClass(),
'meta' => [
'seat_count' => 0,
'template_count' => 0,
'cache_hit' => false,
'computed_at' => time(),
],
]);
}
// 调用 QueryManager 构建数据
$treeData = QueryManager::buildTree($goodsId, $groupBy, $seatSpecMap);
$templateList = QueryManager::buildTemplatePool($goodsId, $treeData['template_keys'] ?? []);
$seatTemplates = QueryManager::transformTemplatePool($templateList);
// 提取场次元数据(从 SKU extends JSON供前端场次选择控件使用禁用判断 + 倒计时)
$sessionMeta = self::extractSessionMeta($goodsId);
// 组装响应
$result = [
'goods_id' => $goodsId,
'group_by' => $groupBy,
'tree' => $treeData['tree'],
'seat_templates' => $seatTemplates,
'session_meta' => $sessionMeta,
'peer_goods' => QueryManager::getPeerGoods($goodsId),
'meta' => [
'seat_count' => $treeData['seat_count'] ?? 0,
'template_count' => count($seatTemplates),
'cache_hit' => false,
'computed_at' => time(),
],
];
// 写入缓存TTL = 60s
\think\facade\Cache::set($cacheKey, $result, 60);
return self::success($result);
} catch (\Exception $e) {
return self::error('获取层级树失败: ' . $e->getMessage());
}
}
/**
* 格式化商品列表数据
*/
private static function formatGoodsList($result)
{
$list = [];
if (!empty($result)) {
foreach ($result as $goods) {
$list[] = [
'id' => $goods['id'],
'title' => $goods['title'],
'image' => $goods['image'],
'price' => $goods['price'],
'original_price' => $goods['original_price'] ?? $goods['price'],
'sales' => $goods['sales'] ?? 0,
'stock' => $goods['stock'] ?? 0,
'venue' => isset($goods['produce_venue']) ? $goods['produce_venue'] : '',
'date' => isset($goods['batch_number_expire']) && $goods['batch_number_expire'] > 0 ? date('Y-m-d', $goods['batch_number_expire']) : '',
'add_time' => $goods['add_time'] ?? '',
];
}
}
return $list;
}
/**
* 格式化商品详情数据
*/
private static function formatGoodsDetail($goods)
{
return [
'id' => $goods['id'],
'title' => $goods['title'],
'image' => $goods['image'],
'images' => !empty($goods['images']) ? explode(',', $goods['images']) : [$goods['image']],
'price' => $goods['price'],
'original_price' => $goods['original_price'] ?? $goods['price'],
'sales' => $goods['sales'] ?? 0,
'stock' => $goods['stock'] ?? 0,
'content' => htmlspecialchars_decode($goods['content'] ?? ''),
'spec_type' => $goods['spec_type'] ?? 0,
'spec_value_id' => $goods['spec_value_id'] ?? '',
// 票务相关字段
'venue' => $goods['produce_venue'] ?? '',
'date' => isset($goods['batch_number_expire']) && $goods['batch_number_expire'] > 0 ? date('Y-m-d', $goods['batch_number_expire']) : '',
'time' => $goods['produce_time'] ?? '',
'region' => $goods['produce_region'] ?? '',
'add_time' => $goods['add_time'] ?? '',
];
}
/**
* 从 SKU 的 extends JSON 中提取场次元数据(去重)
*
* 供前端场次选择控件使用:
* - 判断场次是否已过期disabled
* - 显示停售倒计时
*
* @param int $goodsId
* @return array [['session' => '19:30-21:30', 'start' => '19:30', 'end' => '21:30', 'session_date' => '2026-05-18', 'session_datetime' => '2026-05-18 19:30:00', 'batch_expire_ts' => 1747567200], ...]
*/
private static function extractSessionMeta(int $goodsId): array
{
$specs = \think\facade\Db::name('GoodsSpecBase')
->where('goods_id', $goodsId)
->select()
->toArray();
if (empty($specs)) {
return [];
}
$seen = [];
$result = [];
foreach ($specs as $spec) {
$extends = json_decode($spec['extends'] ?? '{}', true);
$session = $extends['session_start'] ?? '';
$sessionEnd = $extends['session_end'] ?? '';
if (empty($session)) {
continue;
}
$sessionStr = "{$session}-{$sessionEnd}";
// 去重:同一场次只保留一条
if (isset($seen[$sessionStr])) {
continue;
}
$seen[$sessionStr] = true;
$result[] = [
'session' => $sessionStr,
'start' => $extends['session_start'] ?? '',
'end' => $extends['session_end'] ?? '',
'session_date' => $extends['session_date'] ?? '',
'session_datetime' => $extends['session_datetime'] ?? '',
'batch_expire_ts' => intval($extends['batch_expire_ts'] ?? 0),
];
}
// 按场次时间排序
usort($result, function ($a, $b) {
return strcmp($a['session'], $b['session']);
});
return $result;
}
}