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

502 lines
17 KiB
PHP
Raw Normal View History

<?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 {
// 缓存检查(注意:旧缓存不含 goods 字段,需要补充)
$cacheKey = 'vr_tree_v4_' . $goodsId . '_' . md5(implode(',', $groupBy));
$cached = \think\facade\Cache::get($cacheKey);
if ($cached !== null) {
$cached['meta']['cache_hit'] = true;
// 旧缓存不含 goods补充构建
if (!isset($cached['goods'])) {
$cached['goods'] = self::buildGoodsField($goodsId);
}
return self::success($cached);
}
// 读取数据源
$seatMapData = SeatMapService::GetSeatMap($goodsId);
$seatSpecMap = $seatMapData['seatSpecMap'] ?? [];
if (empty($seatSpecMap)) {
return self::success([
'goods_id' => $goodsId,
'group_by' => $groupBy,
'tree' => [],
'goods' => self::buildGoodsField($goodsId),
'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'],
'goods' => self::buildGoodsField($goodsId),
'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());
}
}
/**
* 构建 goods 字段(包含手机详情 content_app
*
* 复用 GoodsService::GoodsList传入 is_content_app=1 以包含手机端详情数据。
* 这使得 uniapp 可以仅调用 tree API 一次就获取完整数据,无需二次请求 goods/detail。
*
* @param int $goodsId
* @return array
*/
private static function buildGoodsField(int $goodsId): array
{
$isUseMobileDetail = intval(\MyC('common_app_is_use_mobile_detail', 0, true));
$params = [
'where' => [
['id', '=', $goodsId],
['is_delete_time', '=', 0],
],
'is_content_app' => $isUseMobileDetail,
'is_spec' => 1,
'is_params' => 1,
'is_favor' => 1,
'm' => 0,
'n' => 1,
];
$ret = GoodsService::GoodsList($params);
$goods = $ret['data'][0] ?? null;
if (empty($goods)) {
return [];
}
// 手机端详情模式下移除 PC 富文本,减少响应体积
if ($isUseMobileDetail == 1) {
unset($goods['content_web']);
}
return $goods;
}
/**
* 格式化商品列表数据
*/
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;
}
}