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/phase4-ticket-wallet
parent
3633bd84d5
commit
d85eb8e19d
|
|
@ -19,7 +19,9 @@ class Hook
|
|||
|
||||
// 订单支付成功处理
|
||||
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);
|
||||
BaseService::log('Hook::handle result', ['ret' => $ret], 'info');
|
||||
break;
|
||||
|
||||
case 'plugins_service_order_detail_page_info':
|
||||
|
|
@ -27,6 +29,11 @@ class Hook
|
|||
$ret = $this->InjectTicketCard($params);
|
||||
break;
|
||||
|
||||
case 'plugins_view_user_various_bottom':
|
||||
// C端用户中心底部挂载票夹入口
|
||||
$ret = $this->InjectWalletLink($params);
|
||||
break;
|
||||
|
||||
case 'plugins_service_order_delete_success':
|
||||
// 如果有删除拦截等
|
||||
break;
|
||||
|
|
@ -122,7 +129,9 @@ class Hook
|
|||
return;
|
||||
}
|
||||
|
||||
$userId = session('user_id');
|
||||
// 获取当前登录用户(ShopXO 标准方式)
|
||||
$user = \app\service\UserService::LoginUserInfo();
|
||||
$userId = empty($user) ? null : $user['id'];
|
||||
if (empty($userId)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -190,7 +199,7 @@ class Hook
|
|||
// JS
|
||||
$js = '<script>
|
||||
(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) . '";
|
||||
window.VrTicketWallet = {
|
||||
viewTicket: function(ticketId) {
|
||||
|
|
@ -237,5 +246,24 @@ class Hook
|
|||
</script>';
|
||||
$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;
|
||||
|
||||
require_once __DIR__ . '/BaseService.php';
|
||||
|
||||
class TicketService extends BaseService
|
||||
{
|
||||
/**
|
||||
|
|
@ -22,7 +24,8 @@ class TicketService extends BaseService
|
|||
*/
|
||||
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)) {
|
||||
BaseService::log('onOrderPaid: empty order_id', $params, 'warning');
|
||||
return false;
|
||||
|
|
@ -38,7 +41,7 @@ class TicketService extends BaseService
|
|||
// 查询订单明细(规格信息存储在 spec JSON 字段)
|
||||
$order_goods = \think\facade\Db::name('order_detail')
|
||||
->where('order_id', $order_id)
|
||||
->select();
|
||||
->select()->toArray();
|
||||
|
||||
if (empty($order_goods)) {
|
||||
BaseService::log('onOrderPaid: no order detail', ['order_id' => $order_id], 'error');
|
||||
|
|
@ -57,31 +60,50 @@ class TicketService extends BaseService
|
|||
$spec_name = '';
|
||||
$spec_base_id = 0;
|
||||
|
||||
// 完整解析 5 维规格
|
||||
$parsed = [
|
||||
'session' => '',
|
||||
'venue' => '',
|
||||
'studio' => '',
|
||||
'section' => '',
|
||||
'seat' => '',
|
||||
];
|
||||
if (is_array($spec_list)) {
|
||||
// 优先取座位号,其次分区名
|
||||
foreach ($spec_list as $spec_item) {
|
||||
$type = $spec_item['type'] ?? '';
|
||||
$type = $spec_item['type'] ?? '';
|
||||
$value = $spec_item['value'] ?? '';
|
||||
if ($type === '$vr-座位号') {
|
||||
$spec_name = $value;
|
||||
break;
|
||||
} elseif ($type === '$vr-分区' && !$spec_name) {
|
||||
$spec_name = $value;
|
||||
switch ($type) {
|
||||
case '$vr-场次': $parsed['session'] = $value; break;
|
||||
case '$vr-场馆': $parsed['venue'] = $value; break;
|
||||
case '$vr-演播室': $parsed['studio'] = $value; break;
|
||||
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
|
||||
if ($spec_name) {
|
||||
$spec_base = \think\facade\Db::name('goods_spec_value')
|
||||
->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;
|
||||
$og['_parsed_spec_name'] = $parsed['seat'] ?: $parsed['section'];
|
||||
$og['_parsed_seat_info'] = $seat_info;
|
||||
$og['_parsed_goods_snapshot'] = $goods_snapshot;
|
||||
}
|
||||
unset($og);
|
||||
|
||||
|
|
@ -113,6 +135,12 @@ class TicketService extends BaseService
|
|||
{
|
||||
$spec_name = $og['_parsed_spec_name'] ?? '';
|
||||
$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 幂等保护:同一订单+同一座位名只发一张票
|
||||
$existing = \think\facade\Db::name(BaseService::table('tickets'))
|
||||
|
|
@ -135,15 +163,11 @@ class TicketService extends BaseService
|
|||
'order_id' => $order['id'],
|
||||
'order_no' => $order['order_no'],
|
||||
'goods_id' => $og['goods_id'],
|
||||
'goods_snapshot' => json_encode([
|
||||
'goods_name' => $og['title'] ?? '',
|
||||
'spec_name' => $spec_name,
|
||||
'price' => $og['price'] ?? 0,
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'goods_snapshot' => $goods_snapshot,
|
||||
'user_id' => $order['user_id'],
|
||||
'ticket_code' => $ticket_code,
|
||||
'qr_data' => '', // 占位,生成后更新
|
||||
'seat_info' => $spec_name,
|
||||
'seat_info' => $seat_info,
|
||||
'spec_base_id' => $spec_base_id,
|
||||
'real_name' => '',
|
||||
'phone' => '',
|
||||
|
|
|
|||
|
|
@ -27,15 +27,10 @@ class WalletService extends BaseService
|
|||
*/
|
||||
public static function getUserTickets(int $userId): array
|
||||
{
|
||||
// 查询该用户的所有票(关联订单)
|
||||
// 直接查询 tickets 表(user_id 已存在)
|
||||
$tickets = \think\facade\Db::name('vr_tickets')
|
||||
->alias('t')
|
||||
->join('order o', 't.order_id = o.id', 'LEFT')
|
||||
->where('o.user_id', $userId)
|
||||
->where('o.pay_status', 1) // 已支付
|
||||
->where('o.status', '<>', 3) // 未删除
|
||||
->field('t.*')
|
||||
->order('t.issued_at', 'desc')
|
||||
->where('user_id', $userId)
|
||||
->order('issued_at', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
|
|
@ -58,8 +53,33 @@ class WalletService extends BaseService
|
|||
// 生成短码
|
||||
$shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']);
|
||||
|
||||
// 解析座位信息(从 seat_info 中提取场次/场馆)
|
||||
// 优先从 seat_info 解析(5维 pipe 格式),兜底从 goods_snapshot 解析
|
||||
$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[] = [
|
||||
'id' => $ticket['id'],
|
||||
|
|
@ -88,12 +108,10 @@ class WalletService extends BaseService
|
|||
*/
|
||||
public static function getTicketDetail(int $ticketId, int $userId): ?array
|
||||
{
|
||||
// 直接查询 tickets 表(包含 user_id)
|
||||
$ticket = \think\facade\Db::name('vr_tickets')
|
||||
->alias('t')
|
||||
->join('order o', 't.order_id = o.id', 'LEFT')
|
||||
->where('t.id', $ticketId)
|
||||
->where('o.user_id', $userId)
|
||||
->field('t.*')
|
||||
->where('id', $ticketId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
if (empty($ticket)) {
|
||||
|
|
@ -103,6 +121,24 @@ class WalletService extends BaseService
|
|||
// 获取商品信息
|
||||
$goods = \think\facade\Db::name('Goods')->find($ticket['goods_id']);
|
||||
$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']);
|
||||
|
|
|
|||
Loading…
Reference in New Issue