feat(Phase 2): 完成票务商品前端展示层
- Goods.php: item_type=ticket 时加载 ticket_detail.html 模板 - SeatSkuService.php: 新增 GetGoodsViewData() 供前端模板使用 - TicketService.php: onOrderPaid 改用 sxo_order_detail 表 + JSON spec 解析 关联: Phase 2 前台展示层完成council/SecurityEngineer
parent
dc63cff77c
commit
7bd8967648
|
|
@ -135,6 +135,19 @@ class Goods extends Common
|
||||||
MyViewAssign($assign);
|
MyViewAssign($assign);
|
||||||
// 钩子
|
// 钩子
|
||||||
$this->PluginsHook($goods_id, $goods);
|
$this->PluginsHook($goods_id, $goods);
|
||||||
|
|
||||||
|
// 票务商品:加载自定义模板并注入座位数据
|
||||||
|
if (($goods['item_type'] ?? '') === 'ticket') {
|
||||||
|
$viewData = \app\plugins\vr_ticket\service\SeatSkuService::GetGoodsViewData($goods_id);
|
||||||
|
MyViewAssign([
|
||||||
|
'vr_seat_template' => $viewData['vr_seat_template'] ?? null,
|
||||||
|
'goods_spec_data' => $viewData['goods_spec_data'] ?? [],
|
||||||
|
]);
|
||||||
|
// 使用绝对路径 + fetch() 方法,让 Think 驱动正确解析 include 路径
|
||||||
|
$tplFile = ROOT . 'app' . DS . 'plugins' . DS . 'vr_ticket' . DS . 'view' . DS . 'goods' . DS . 'ticket_detail.html';
|
||||||
|
return \think\facade\View::fetch($tplFile, $assign);
|
||||||
|
}
|
||||||
|
|
||||||
return MyView();
|
return MyView();
|
||||||
}
|
}
|
||||||
MyViewAssign('msg', MyLang('goods.goods_no_data_tips'));
|
MyViewAssign('msg', MyLang('goods.goods_no_data_tips'));
|
||||||
|
|
|
||||||
|
|
@ -348,4 +348,102 @@ class SeatSkuService extends BaseService
|
||||||
|
|
||||||
return ['code' => 0];
|
return ['code' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商品前端展示数据(供 ticket_detail.html 模板使用)
|
||||||
|
*
|
||||||
|
* @param int $goodsId
|
||||||
|
* @return array ['vr_seat_template' => [...], 'goods_spec_data' => [...]]
|
||||||
|
*/
|
||||||
|
public static function GetGoodsViewData(int $goodsId): array
|
||||||
|
{
|
||||||
|
// 读取 vr_goods_config
|
||||||
|
$goods = \think\facade\Db::name('goods')->find($goodsId);
|
||||||
|
$vrGoodsConfig = json_decode($goods['vr_goods_config'] ?? '', true);
|
||||||
|
|
||||||
|
if (empty($vrGoodsConfig) || !is_array($vrGoodsConfig)) {
|
||||||
|
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取第一个配置块(单模板模式)
|
||||||
|
$config = $vrGoodsConfig[0];
|
||||||
|
$templateId = intval($config['template_id'] ?? 0);
|
||||||
|
if ($templateId <= 0) {
|
||||||
|
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取座位模板(包含 seat_map 和 spec_base_id_map)
|
||||||
|
$seatTemplate = \think\facade\Db::name(self::table('seat_templates'))
|
||||||
|
->where('id', $templateId)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
// 解码 seat_map JSON(存储时是 JSON 字符串)
|
||||||
|
if (!empty($seatTemplate['seat_map'])) {
|
||||||
|
$decoded = json_decode($seatTemplate['seat_map'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$seatTemplate['seat_map'] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码 spec_base_id_map JSON
|
||||||
|
if (!empty($seatTemplate['spec_base_id_map'])) {
|
||||||
|
$decoded = json_decode($seatTemplate['spec_base_id_map'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$seatTemplate['spec_base_id_map'] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建场次列表(goods_spec_data)
|
||||||
|
$sessions = $config['sessions'] ?? [];
|
||||||
|
$goodsSpecData = [];
|
||||||
|
|
||||||
|
foreach ($sessions as $session) {
|
||||||
|
$start = $session['start'] ?? '';
|
||||||
|
$end = $session['end'] ?? '';
|
||||||
|
$timeRange = $start && $end ? "{$start}-{$end}" : ($start ?: $end);
|
||||||
|
|
||||||
|
// 查找该场次对应的 spec_base_id
|
||||||
|
$specValue = \think\facade\Db::name('goods_spec_value')
|
||||||
|
->alias('sv')
|
||||||
|
->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id')
|
||||||
|
->where('sv.goods_id', $goodsId)
|
||||||
|
->where('sv.value', $timeRange)
|
||||||
|
->where('sb.price', '>', 0)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
$goodsSpecData[] = [
|
||||||
|
'spec_id' => $specValue['goods_spec_base_id'] ?? 0,
|
||||||
|
'spec_name' => $timeRange,
|
||||||
|
'price' => $specValue['price'] ?? floatval($goods['price'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有从配置读取到场次,尝试从数据库直接读取场次类规格值
|
||||||
|
if (empty($goodsSpecData)) {
|
||||||
|
$sessionValues = \think\facade\Db::name('goods_spec_value')
|
||||||
|
->alias('sv')
|
||||||
|
->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id')
|
||||||
|
->field('sv.goods_spec_base_id as spec_id, sv.value as spec_name, sb.price')
|
||||||
|
->where('sv.goods_id', $goodsId)
|
||||||
|
->where('sb.price', '>', 0)
|
||||||
|
->order('sb.id asc')
|
||||||
|
->select()->toArray();
|
||||||
|
|
||||||
|
foreach ($sessionValues as $sv) {
|
||||||
|
if (preg_match('/^\d{2}:\d{2}-\d{2}:\d{2}$/', $sv['spec_name'])) {
|
||||||
|
$goodsSpecData[] = [
|
||||||
|
'spec_id' => $sv['spec_id'],
|
||||||
|
'spec_name' => $sv['spec_name'],
|
||||||
|
'price' => floatval($sv['price']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'vr_seat_template' => $seatTemplate ?: null,
|
||||||
|
'goods_spec_data' => $goodsSpecData,
|
||||||
|
'goods_config' => $config,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class TicketService extends BaseService
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单
|
// 查询订单
|
||||||
$order = \think\facade\Db::name('Order')->find($order_id);
|
$order = \think\facade\Db::name('order')->find($order_id);
|
||||||
if (empty($order) || $order['pay_status'] != 1) {
|
if (empty($order) || $order['pay_status'] != 1) {
|
||||||
BaseService::log('onOrderPaid: order not paid or not found', ['order_id' => $order_id], 'warning');
|
BaseService::log('onOrderPaid: order not paid or not found', ['order_id' => $order_id], 'warning');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -41,16 +41,51 @@ class TicketService extends BaseService
|
||||||
return true; // 不是票务商品,不报错
|
return true; // 不是票务商品,不报错
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询商品快照(规格信息)
|
// 查询订单明细(规格信息存储在 spec JSON 字段)
|
||||||
$order_goods = \think\facade\Db::name('OrderGoods')
|
$order_goods = \think\facade\Db::name('order_detail')
|
||||||
->where('order_id', $order_id)
|
->where('order_id', $order_id)
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
if (empty($order_goods)) {
|
if (empty($order_goods)) {
|
||||||
BaseService::log('onOrderPaid: no order goods', ['order_id' => $order_id], 'error');
|
BaseService::log('onOrderPaid: no order detail', ['order_id' => $order_id], 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 逐个生成票(每个规格选项 = 一张票)
|
// 逐行解析 spec JSON,提取座位信息
|
||||||
|
foreach ($order_goods as &$og) {
|
||||||
|
$spec_list = json_decode($og['spec'] ?? '[]', true);
|
||||||
|
$spec_name = '';
|
||||||
|
$spec_base_id = 0;
|
||||||
|
|
||||||
|
if (is_array($spec_list)) {
|
||||||
|
// 优先取座位号,其次分区名
|
||||||
|
foreach ($spec_list as $spec_item) {
|
||||||
|
$type = $spec_item['type'] ?? '';
|
||||||
|
$value = $spec_item['value'] ?? '';
|
||||||
|
if ($type === '$vr-座位号') {
|
||||||
|
$spec_name = $value;
|
||||||
|
break;
|
||||||
|
} elseif ($type === '$vr-分区' && !$spec_name) {
|
||||||
|
$spec_name = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试通过座位名反向查找 spec_base_id
|
||||||
|
if ($spec_name) {
|
||||||
|
$spec_base = \think\facade\Db::name('goods_spec_value')
|
||||||
|
->where('goods_id', $order['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);
|
||||||
|
|
||||||
|
// 逐个生成票(每个订单明细行 = 一张票)
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($order_goods as $og) {
|
foreach ($order_goods as $og) {
|
||||||
$ticket_id = self::issueTicket($order, $og);
|
$ticket_id = self::issueTicket($order, $og);
|
||||||
|
|
@ -71,20 +106,23 @@ class TicketService extends BaseService
|
||||||
* 发放单张票
|
* 发放单张票
|
||||||
*
|
*
|
||||||
* @param array $order 订单数据
|
* @param array $order 订单数据
|
||||||
* @param array $order_goods 订单商品数据(包含 spec_base_id)
|
* @param array $og 订单商品数据(来自 vrt_order_detail,已解析 _parsed_spec_name 等字段)
|
||||||
* @return int 票ID
|
* @return int 票ID
|
||||||
*/
|
*/
|
||||||
public static function issueTicket($order, $order_goods)
|
public static function issueTicket($order, $og)
|
||||||
{
|
{
|
||||||
// P0-1 幂等保护:同一订单+同一规格只发一张票
|
$spec_name = $og['_parsed_spec_name'] ?? '';
|
||||||
|
$spec_base_id = $og['_parsed_spec_base_id'] ?? 0;
|
||||||
|
|
||||||
|
// P0-1 幂等保护:同一订单+同一座位名只发一张票
|
||||||
$existing = \think\facade\Db::name(BaseService::table('tickets'))
|
$existing = \think\facade\Db::name(BaseService::table('tickets'))
|
||||||
->where('order_id', $order['id'])
|
->where('order_id', $order['id'])
|
||||||
->where('spec_base_id', $order_goods['spec_base_id'] ?? 0)
|
->where('seat_info', $spec_name)
|
||||||
->find();
|
->find();
|
||||||
if (!empty($existing)) {
|
if (!empty($existing)) {
|
||||||
BaseService::log('issueTicket: idempotent_skip', [
|
BaseService::log('issueTicket: idempotent_skip', [
|
||||||
'order_id' => $order['id'],
|
'order_id' => $order['id'],
|
||||||
'spec_base_id'=> $order_goods['spec_base_id'] ?? 0,
|
'seat_info' => $spec_name,
|
||||||
], 'info');
|
], 'info');
|
||||||
return $existing['id'];
|
return $existing['id'];
|
||||||
}
|
}
|
||||||
|
|
@ -96,11 +134,11 @@ class TicketService extends BaseService
|
||||||
'id' => 0, // 写入后再更新
|
'id' => 0, // 写入后再更新
|
||||||
'code' => $ticket_code,
|
'code' => $ticket_code,
|
||||||
'event'=> $order['goods_id'],
|
'event'=> $order['goods_id'],
|
||||||
'seat' => $order_goods['spec_name'] ?? '', // 规格名=座位信息
|
'seat' => $spec_name,
|
||||||
];
|
];
|
||||||
$qr_data = BaseService::encryptQrData($qr_payload);
|
$qr_data = BaseService::encryptQrData($qr_payload);
|
||||||
|
|
||||||
// 观演人信息(从订单扩展字段读取,由购票页表单写入)
|
// 观演人信息:优先从 order.extension_data 读取(购票页表单写入)
|
||||||
$extension_data = json_decode($order['extension_data'] ?? '{}', true);
|
$extension_data = json_decode($order['extension_data'] ?? '{}', true);
|
||||||
$attendee = $extension_data['attendee'] ?? [];
|
$attendee = $extension_data['attendee'] ?? [];
|
||||||
|
|
||||||
|
|
@ -111,19 +149,19 @@ class TicketService extends BaseService
|
||||||
'order_no' => $order['order_no'],
|
'order_no' => $order['order_no'],
|
||||||
'goods_id' => $order['goods_id'],
|
'goods_id' => $order['goods_id'],
|
||||||
'goods_snapshot' => json_encode([
|
'goods_snapshot' => json_encode([
|
||||||
'goods_name' => $order['goods_name'] ?? '',
|
'goods_name' => $og['title'] ?? '',
|
||||||
'spec_name' => $order_goods['spec_name'] ?? '',
|
'spec_name' => $spec_name,
|
||||||
'price' => $order_goods['goods_price'] ?? 0,
|
'price' => $og['price'] ?? 0,
|
||||||
], JSON_UNESCAPED_UNICODE),
|
], 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,
|
'qr_data' => $qr_data,
|
||||||
'seat_info' => $order_goods['spec_name'] ?? '',
|
'seat_info' => $spec_name,
|
||||||
'spec_base_id' => $order_goods['spec_base_id'] ?? 0,
|
'spec_base_id' => $spec_base_id,
|
||||||
'real_name' => $attendee['real_name'] ?? '',
|
'real_name' => $attendee['real_name'] ?? '',
|
||||||
'phone' => $attendee['phone'] ?? '',
|
'phone' => $attendee['phone'] ?? '',
|
||||||
'id_card' => $attendee['id_card'] ?? '',
|
'id_card' => $attendee['id_card'] ?? '',
|
||||||
'verify_status' => 0, // 0=未核销
|
'verify_status' => 0,
|
||||||
'issued_at' => $now,
|
'issued_at' => $now,
|
||||||
'created_at' => $now,
|
'created_at' => $now,
|
||||||
'updated_at' => $now,
|
'updated_at' => $now,
|
||||||
|
|
@ -201,14 +239,14 @@ class TicketService extends BaseService
|
||||||
'verifier_id' => $verifier_id,
|
'verifier_id' => $verifier_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 审计日志(失败也记录,便于追溯异常)
|
// 审计日志
|
||||||
AuditService::logVerify(
|
AuditService::logVerify(
|
||||||
$ticket['id'],
|
$ticket['id'],
|
||||||
$ticket_code,
|
$ticket_code,
|
||||||
$verifier_id,
|
$verifier_id,
|
||||||
$verifier['name'] ?? '',
|
$verifier['name'] ?? '',
|
||||||
'success',
|
'success',
|
||||||
0 // 原状态(核销前一定是 0)
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -239,7 +277,6 @@ class TicketService extends BaseService
|
||||||
if ($status !== null) {
|
if ($status !== null) {
|
||||||
$where['verify_status'] = $status;
|
$where['verify_status'] = $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
return \think\facade\Db::name(BaseService::table('tickets'))
|
return \think\facade\Db::name(BaseService::table('tickets'))
|
||||||
->where($where)
|
->where($where)
|
||||||
->order('created_at', 'desc')
|
->order('created_at', 'desc')
|
||||||
|
|
@ -248,9 +285,6 @@ class TicketService extends BaseService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 QR 码图片 URL
|
* 生成 QR 码图片 URL
|
||||||
*
|
|
||||||
* @param string $ticket_code
|
|
||||||
* @return string QR码图片URL
|
|
||||||
*/
|
*/
|
||||||
public static function getQrCodeUrl($ticket_code)
|
public static function getQrCodeUrl($ticket_code)
|
||||||
{
|
{
|
||||||
|
|
@ -258,7 +292,6 @@ class TicketService extends BaseService
|
||||||
'type' => 'vr_ticket',
|
'type' => 'vr_ticket',
|
||||||
'code' => $ticket_code,
|
'code' => $ticket_code,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
return request()->domain() . request()->root() . '?s=index/qrcode/index&content=' . urlencode($content) . '&size=8&level=H';
|
return request()->domain() . request()->root() . '?s=index/qrcode/index&content=' . urlencode($content) . '&size=8&level=H';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue