2026-05-14 04:58:05 +00:00
|
|
|
|
<?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;
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
use app\plugins\vr_ticket\service\QueryManager;
|
2026-05-14 04:58:05 +00:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取层级树 API(Tree 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 {
|
2026-06-13 02:56:40 +00:00
|
|
|
|
// 缓存检查(注意:旧缓存不含 goods 字段,需要补充)
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
$cacheKey = 'vr_tree_v4_' . $goodsId . '_' . md5(implode(',', $groupBy));
|
|
|
|
|
|
$cached = \think\facade\Cache::get($cacheKey);
|
|
|
|
|
|
if ($cached !== null) {
|
|
|
|
|
|
$cached['meta']['cache_hit'] = true;
|
2026-06-13 02:56:40 +00:00
|
|
|
|
// 旧缓存不含 goods,补充构建
|
|
|
|
|
|
if (!isset($cached['goods'])) {
|
|
|
|
|
|
$cached['goods'] = self::buildGoodsField($goodsId);
|
|
|
|
|
|
}
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
return self::success($cached);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 读取数据源
|
|
|
|
|
|
$seatMapData = SeatMapService::GetSeatMap($goodsId);
|
|
|
|
|
|
$seatSpecMap = $seatMapData['seatSpecMap'] ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($seatSpecMap)) {
|
|
|
|
|
|
return self::success([
|
|
|
|
|
|
'goods_id' => $goodsId,
|
|
|
|
|
|
'group_by' => $groupBy,
|
|
|
|
|
|
'tree' => [],
|
2026-06-13 02:56:40 +00:00
|
|
|
|
'goods' => self::buildGoodsField($goodsId),
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
'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'],
|
2026-06-13 02:56:40 +00:00
|
|
|
|
'goods' => self::buildGoodsField($goodsId),
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
'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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 02:56:40 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 构建 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 04:58:05 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 格式化商品列表数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
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'] : '',
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
'date' => isset($goods['batch_number_expire']) && $goods['batch_number_expire'] > 0 ? date('Y-m-d', $goods['batch_number_expire']) : '',
|
2026-05-14 04:58:05 +00:00
|
|
|
|
'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'] ?? '',
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
'date' => isset($goods['batch_number_expire']) && $goods['batch_number_expire'] > 0 ? date('Y-m-d', $goods['batch_number_expire']) : '',
|
2026-05-14 04:58:05 +00:00
|
|
|
|
'time' => $goods['produce_time'] ?? '',
|
|
|
|
|
|
'region' => $goods['produce_region'] ?? '',
|
|
|
|
|
|
'add_time' => $goods['add_time'] ?? '',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
feat: VR ticket plugin full update - Tree API, City selector, seatmap service, wallet plugin
Core changes:
- Add Tree API design docs + implementation plan (14/15/16_*.md)
- Add CityService + City API (city selector component)
- Enhance SeatMapService + SeatSkuService (seatmap.test.json)
- Add Hook.php event injection, AdminGoodsSave hooks
- Update ticket_detail.html, venue/save.html views
- Add CORS middleware, Weixin payment fix
Wallet plugin (new submodule):
- Full wallet plugin: admin (Cash/Recharge/Transfer/Config)
- API layer: User/Walletlog/Recharge/Cash/Transfer endpoints
- Hook integration
Docs:
- VR_TICKET_TREE_API.md, VR_TICKET_WALLET_VERIFY_API.md
- Council evaluation report, test files
Tools:
- strip_batch.py, strip_page.py (cleanup utilities)
(153 files, +27784/-686 lines)
2026-06-03 05:34:38 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 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;
|
|
|
|
|
|
}
|
2026-05-14 04:58:05 +00:00
|
|
|
|
}
|