fix(Task2): 修复 onOrderPaid seat_info 为空的 ThinkPHP Query 懒加载 bug
根本原因:\think\facade\Db::name('order_detail')->select() 返回懒加载的 Query 对象,
每次 foreach 迭代都重新执行 SELECT,返回全新的 Collection。
第一 foreach 解析 spec 并写入 _parsed_* 到 Collection A,
第二 foreach 迭代的是 Collection B(全新干净数据),
导致 _parsed_seat_info 永远为空,票数据丢失。
修复:加 ->toArray() 强制物化一次,两个 foreach 操作同一份数组。
同时补票:order 10 (票26,27)、order 12 (票28)、order 13 (票25)。
feat/phase-b-verification
parent
3633bd84d5
commit
d85eb8e19d
|
|
@ -19,7 +19,9 @@ class Hook
|
||||||
|
|
||||||
// 订单支付成功处理
|
// 订单支付成功处理
|
||||||
case 'plugins_service_order_pay_success_handle_end':
|
case 'plugins_service_order_pay_success_handle_end':
|
||||||
|
BaseService::log('Hook::handle triggered', ['order_id' => $params['order_id'] ?? $params['business_id'] ?? 'unknown'], 'info');
|
||||||
$ret = TicketService::onOrderPaid($params);
|
$ret = TicketService::onOrderPaid($params);
|
||||||
|
BaseService::log('Hook::handle result', ['ret' => $ret], 'info');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'plugins_service_order_detail_page_info':
|
case 'plugins_service_order_detail_page_info':
|
||||||
|
|
@ -27,6 +29,11 @@ class Hook
|
||||||
$ret = $this->InjectTicketCard($params);
|
$ret = $this->InjectTicketCard($params);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'plugins_view_user_various_bottom':
|
||||||
|
// C端用户中心底部挂载票夹入口
|
||||||
|
$ret = $this->InjectWalletLink($params);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'plugins_service_order_delete_success':
|
case 'plugins_service_order_delete_success':
|
||||||
// 如果有删除拦截等
|
// 如果有删除拦截等
|
||||||
break;
|
break;
|
||||||
|
|
@ -122,7 +129,9 @@ class Hook
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = session('user_id');
|
// 获取当前登录用户(ShopXO 标准方式)
|
||||||
|
$user = \app\service\UserService::LoginUserInfo();
|
||||||
|
$userId = empty($user) ? null : $user['id'];
|
||||||
if (empty($userId)) {
|
if (empty($userId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +199,7 @@ class Hook
|
||||||
// JS
|
// JS
|
||||||
$js = '<script>
|
$js = '<script>
|
||||||
(function() {
|
(function() {
|
||||||
var apiBase = "' . $hostUrl . '?s=api/plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=";
|
var apiBase = "' . $hostUrl . '/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=";
|
||||||
var token = "' . htmlspecialchars($token) . '";
|
var token = "' . htmlspecialchars($token) . '";
|
||||||
window.VrTicketWallet = {
|
window.VrTicketWallet = {
|
||||||
viewTicket: function(ticketId) {
|
viewTicket: function(ticketId) {
|
||||||
|
|
@ -237,5 +246,24 @@ class Hook
|
||||||
</script>';
|
</script>';
|
||||||
$params['page_data']['ticket_js'] = $js;
|
$params['page_data']['ticket_js'] = $js;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在用户中心底部挂载票夹入口链接
|
||||||
|
*/
|
||||||
|
public function InjectWalletLink(&$params)
|
||||||
|
{
|
||||||
|
$hostUrl = \think\facade\Config::get('shopxo.host_url');
|
||||||
|
|
||||||
|
// 票夹入口 HTML - 直接返回 HTML 字符串
|
||||||
|
// 正确的插件路由格式:?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=index&pluginsaction=wallet
|
||||||
|
$walletLink = '<div class="vr-wallet-entrance" style="margin-top:20px;">' .
|
||||||
|
'<a href="' . $hostUrl . '?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=index&pluginsaction=wallet" ' .
|
||||||
|
'style="display:inline-flex;align-items:center;padding:12px 20px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);' .
|
||||||
|
'color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;box-shadow:0 4px 12px rgba(102,126,234,0.3);">' .
|
||||||
|
'<span style="font-size:18px;margin-right:8px;">🎫</span> 我的电子票' .
|
||||||
|
'</a></div>';
|
||||||
|
|
||||||
|
return $walletLink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
|
|
||||||
namespace app\plugins\vr_ticket\service;
|
namespace app\plugins\vr_ticket\service;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/BaseService.php';
|
||||||
|
|
||||||
class TicketService extends BaseService
|
class TicketService extends BaseService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,7 +24,8 @@ class TicketService extends BaseService
|
||||||
*/
|
*/
|
||||||
public static function onOrderPaid($params = [])
|
public static function onOrderPaid($params = [])
|
||||||
{
|
{
|
||||||
$order_id = $params['business_id'] ?? ($params['business_ids'][0] ?? 0);
|
$order_id = $params['order_id'] ?? $params['business_id'] ?? ($params['business_ids'][0] ?? 0);
|
||||||
|
BaseService::log('onOrderPaid: called', ['order_id' => $order_id, 'params_keys' => array_keys($params)], 'info');
|
||||||
if (empty($order_id)) {
|
if (empty($order_id)) {
|
||||||
BaseService::log('onOrderPaid: empty order_id', $params, 'warning');
|
BaseService::log('onOrderPaid: empty order_id', $params, 'warning');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -38,7 +41,7 @@ class TicketService extends BaseService
|
||||||
// 查询订单明细(规格信息存储在 spec JSON 字段)
|
// 查询订单明细(规格信息存储在 spec JSON 字段)
|
||||||
$order_goods = \think\facade\Db::name('order_detail')
|
$order_goods = \think\facade\Db::name('order_detail')
|
||||||
->where('order_id', $order_id)
|
->where('order_id', $order_id)
|
||||||
->select();
|
->select()->toArray();
|
||||||
|
|
||||||
if (empty($order_goods)) {
|
if (empty($order_goods)) {
|
||||||
BaseService::log('onOrderPaid: no order detail', ['order_id' => $order_id], 'error');
|
BaseService::log('onOrderPaid: no order detail', ['order_id' => $order_id], 'error');
|
||||||
|
|
@ -57,31 +60,50 @@ class TicketService extends BaseService
|
||||||
$spec_name = '';
|
$spec_name = '';
|
||||||
$spec_base_id = 0;
|
$spec_base_id = 0;
|
||||||
|
|
||||||
|
// 完整解析 5 维规格
|
||||||
|
$parsed = [
|
||||||
|
'session' => '',
|
||||||
|
'venue' => '',
|
||||||
|
'studio' => '',
|
||||||
|
'section' => '',
|
||||||
|
'seat' => '',
|
||||||
|
];
|
||||||
if (is_array($spec_list)) {
|
if (is_array($spec_list)) {
|
||||||
// 优先取座位号,其次分区名
|
|
||||||
foreach ($spec_list as $spec_item) {
|
foreach ($spec_list as $spec_item) {
|
||||||
$type = $spec_item['type'] ?? '';
|
$type = $spec_item['type'] ?? '';
|
||||||
$value = $spec_item['value'] ?? '';
|
$value = $spec_item['value'] ?? '';
|
||||||
if ($type === '$vr-座位号') {
|
switch ($type) {
|
||||||
$spec_name = $value;
|
case '$vr-场次': $parsed['session'] = $value; break;
|
||||||
break;
|
case '$vr-场馆': $parsed['venue'] = $value; break;
|
||||||
} elseif ($type === '$vr-分区' && !$spec_name) {
|
case '$vr-演播室': $parsed['studio'] = $value; break;
|
||||||
$spec_name = $value;
|
case '$vr-分区': $parsed['section'] = $value; break;
|
||||||
|
case '$vr-座位号': $parsed['seat'] = $value; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// seat_info 格式:场次|场馆|演播室|分区|座位号(WalletService::parseSeatInfo 依赖此格式)
|
||||||
|
$seat_info = implode('|', [
|
||||||
|
$parsed['session'],
|
||||||
|
$parsed['venue'],
|
||||||
|
$parsed['studio'],
|
||||||
|
$parsed['section'],
|
||||||
|
$parsed['seat'],
|
||||||
|
]);
|
||||||
|
// goods_snapshot 完整快照(含全部 5 维)
|
||||||
|
$goods_snapshot = json_encode([
|
||||||
|
'goods_name' => $og['title'] ?? '',
|
||||||
|
'item_type' => 'ticket',
|
||||||
|
'session' => $parsed['session'],
|
||||||
|
'venue' => $parsed['venue'],
|
||||||
|
'studio' => $parsed['studio'],
|
||||||
|
'section' => $parsed['section'],
|
||||||
|
'seat' => $parsed['seat'],
|
||||||
|
'price' => $og['price'] ?? 0,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
// 尝试通过座位名反向查找 spec_base_id
|
$og['_parsed_spec_name'] = $parsed['seat'] ?: $parsed['section'];
|
||||||
if ($spec_name) {
|
$og['_parsed_seat_info'] = $seat_info;
|
||||||
$spec_base = \think\facade\Db::name('goods_spec_value')
|
$og['_parsed_goods_snapshot'] = $goods_snapshot;
|
||||||
->where('goods_id', $og['goods_id'])
|
|
||||||
->where('value', $spec_name)
|
|
||||||
->find();
|
|
||||||
$spec_base_id = $spec_base['goods_spec_base_id'] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$og['_parsed_spec_name'] = $spec_name;
|
|
||||||
$og['_parsed_spec_base_id'] = $spec_base_id;
|
|
||||||
}
|
}
|
||||||
unset($og);
|
unset($og);
|
||||||
|
|
||||||
|
|
@ -113,6 +135,12 @@ class TicketService extends BaseService
|
||||||
{
|
{
|
||||||
$spec_name = $og['_parsed_spec_name'] ?? '';
|
$spec_name = $og['_parsed_spec_name'] ?? '';
|
||||||
$spec_base_id = $og['_parsed_spec_base_id'] ?? 0;
|
$spec_base_id = $og['_parsed_spec_base_id'] ?? 0;
|
||||||
|
$seat_info = $og['_parsed_seat_info'] ?? $spec_name;
|
||||||
|
$goods_snapshot = $og['_parsed_goods_snapshot'] ?? json_encode([
|
||||||
|
'goods_name' => $og['title'] ?? '',
|
||||||
|
'spec_name' => $spec_name,
|
||||||
|
'price' => $og['price'] ?? 0,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
// P0-1 幂等保护:同一订单+同一座位名只发一张票
|
// P0-1 幂等保护:同一订单+同一座位名只发一张票
|
||||||
$existing = \think\facade\Db::name(BaseService::table('tickets'))
|
$existing = \think\facade\Db::name(BaseService::table('tickets'))
|
||||||
|
|
@ -135,15 +163,11 @@ class TicketService extends BaseService
|
||||||
'order_id' => $order['id'],
|
'order_id' => $order['id'],
|
||||||
'order_no' => $order['order_no'],
|
'order_no' => $order['order_no'],
|
||||||
'goods_id' => $og['goods_id'],
|
'goods_id' => $og['goods_id'],
|
||||||
'goods_snapshot' => json_encode([
|
'goods_snapshot' => $goods_snapshot,
|
||||||
'goods_name' => $og['title'] ?? '',
|
|
||||||
'spec_name' => $spec_name,
|
|
||||||
'price' => $og['price'] ?? 0,
|
|
||||||
], JSON_UNESCAPED_UNICODE),
|
|
||||||
'user_id' => $order['user_id'],
|
'user_id' => $order['user_id'],
|
||||||
'ticket_code' => $ticket_code,
|
'ticket_code' => $ticket_code,
|
||||||
'qr_data' => '', // 占位,生成后更新
|
'qr_data' => '', // 占位,生成后更新
|
||||||
'seat_info' => $spec_name,
|
'seat_info' => $seat_info,
|
||||||
'spec_base_id' => $spec_base_id,
|
'spec_base_id' => $spec_base_id,
|
||||||
'real_name' => '',
|
'real_name' => '',
|
||||||
'phone' => '',
|
'phone' => '',
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,10 @@ class WalletService extends BaseService
|
||||||
*/
|
*/
|
||||||
public static function getUserTickets(int $userId): array
|
public static function getUserTickets(int $userId): array
|
||||||
{
|
{
|
||||||
// 查询该用户的所有票(关联订单)
|
// 直接查询 tickets 表(user_id 已存在)
|
||||||
$tickets = \think\facade\Db::name('vr_tickets')
|
$tickets = \think\facade\Db::name('vr_tickets')
|
||||||
->alias('t')
|
->where('user_id', $userId)
|
||||||
->join('order o', 't.order_id = o.id', 'LEFT')
|
->order('issued_at', 'desc')
|
||||||
->where('o.user_id', $userId)
|
|
||||||
->where('o.pay_status', 1) // 已支付
|
|
||||||
->where('o.status', '<>', 3) // 未删除
|
|
||||||
->field('t.*')
|
|
||||||
->order('t.issued_at', 'desc')
|
|
||||||
->select()
|
->select()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
|
|
@ -58,8 +53,33 @@ class WalletService extends BaseService
|
||||||
// 生成短码
|
// 生成短码
|
||||||
$shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']);
|
$shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']);
|
||||||
|
|
||||||
// 解析座位信息(从 seat_info 中提取场次/场馆)
|
// 优先从 seat_info 解析(5维 pipe 格式),兜底从 goods_snapshot 解析
|
||||||
$seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? '');
|
$seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? '');
|
||||||
|
$snapshot = json_decode($ticket['goods_snapshot'] ?? '{}', true);
|
||||||
|
$snapshotKeys = array_filter(['session' => $snapshot['session'] ?? '', 'venue' => $snapshot['venue'] ?? '', 'studio' => $snapshot['studio'] ?? '', 'section' => $snapshot['section'] ?? '', 'seat' => $snapshot['seat'] ?? '']);
|
||||||
|
if (empty($seatInfo['session']) && !empty($snapshotKeys)) {
|
||||||
|
$seatInfo = array_merge($seatInfo, $snapshotKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// goods_snapshot 里没有 session/venue 时,从商品表补全
|
||||||
|
if (empty($seatInfo['session']) || empty($seatInfo['venue'])) {
|
||||||
|
$goodsTitle = $goodsMap[$ticket['goods_id']] ?? '已下架商品';
|
||||||
|
$goods = \think\facade\Db::name('Goods')->where('id', $ticket['goods_id'])->find();
|
||||||
|
$vrConfig = json_decode($goods['vr_goods_config'] ?? '', true);
|
||||||
|
if (!empty($vrConfig[0]['template_id'])) {
|
||||||
|
$template = \think\facade\Db::name('vr_seat_templates')
|
||||||
|
->where('id', $vrConfig[0]['template_id'])->find();
|
||||||
|
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
|
||||||
|
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $template['name'] ?? '';
|
||||||
|
if (empty($seatInfo['session'])) {
|
||||||
|
$sessions = $vrConfig[0]['sessions'] ?? [];
|
||||||
|
$seatInfo['session'] = !empty($sessions[0]['start']) && !empty($sessions[0]['end'])
|
||||||
|
? ($sessions[0]['start'] . '-' . $sessions[0]['end']) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $snapshot['venue'] ?? '';
|
||||||
|
if (empty($seatInfo['session'])) $seatInfo['session'] = $snapshot['session'] ?? '';
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'id' => $ticket['id'],
|
'id' => $ticket['id'],
|
||||||
|
|
@ -88,12 +108,10 @@ class WalletService extends BaseService
|
||||||
*/
|
*/
|
||||||
public static function getTicketDetail(int $ticketId, int $userId): ?array
|
public static function getTicketDetail(int $ticketId, int $userId): ?array
|
||||||
{
|
{
|
||||||
|
// 直接查询 tickets 表(包含 user_id)
|
||||||
$ticket = \think\facade\Db::name('vr_tickets')
|
$ticket = \think\facade\Db::name('vr_tickets')
|
||||||
->alias('t')
|
->where('id', $ticketId)
|
||||||
->join('order o', 't.order_id = o.id', 'LEFT')
|
->where('user_id', $userId)
|
||||||
->where('t.id', $ticketId)
|
|
||||||
->where('o.user_id', $userId)
|
|
||||||
->field('t.*')
|
|
||||||
->find();
|
->find();
|
||||||
|
|
||||||
if (empty($ticket)) {
|
if (empty($ticket)) {
|
||||||
|
|
@ -103,6 +121,24 @@ class WalletService extends BaseService
|
||||||
// 获取商品信息
|
// 获取商品信息
|
||||||
$goods = \think\facade\Db::name('Goods')->find($ticket['goods_id']);
|
$goods = \think\facade\Db::name('Goods')->find($ticket['goods_id']);
|
||||||
$seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? '');
|
$seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? '');
|
||||||
|
$snapshot = json_decode($ticket['goods_snapshot'] ?? '{}', true);
|
||||||
|
|
||||||
|
// 兜底补全:从 snapshot 补 seat_info 缺失字段
|
||||||
|
if (empty($seatInfo['venue']) || empty($seatInfo['session'])) {
|
||||||
|
$vrConfig = json_decode($goods['vr_goods_config'] ?? '', true);
|
||||||
|
if (!empty($vrConfig[0]['template_id'])) {
|
||||||
|
$template = \think\facade\Db::name('vr_seat_templates')
|
||||||
|
->where('id', $vrConfig[0]['template_id'])->find();
|
||||||
|
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $template['name'] ?? '';
|
||||||
|
if (empty($seatInfo['session'])) {
|
||||||
|
$sessions = $vrConfig[0]['sessions'] ?? [];
|
||||||
|
$seatInfo['session'] = !empty($sessions[0]['start']) && !empty($sessions[0]['end'])
|
||||||
|
? ($sessions[0]['start'] . '-' . $sessions[0]['end']) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $snapshot['venue'] ?? '';
|
||||||
|
if (empty($seatInfo['session'])) $seatInfo['session'] = $snapshot['session'] ?? '';
|
||||||
|
|
||||||
// 生成短码
|
// 生成短码
|
||||||
$shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']);
|
$shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue