2026-04-15 05:09:44 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* VR票务插件 - 票务服务
|
|
|
|
|
|
*
|
|
|
|
|
|
* 核心业务:订单支付成功 → 生成电子票
|
|
|
|
|
|
*
|
|
|
|
|
|
* @package vr_ticket\service
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\plugins\vr_ticket\service;
|
|
|
|
|
|
|
2026-04-15 06:20:03 +00:00
|
|
|
|
class TicketService extends BaseService
|
2026-04-15 05:09:44 +00:00
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 订单支付成功回调
|
|
|
|
|
|
*
|
|
|
|
|
|
* 从 plugin.json 的 hook 触发:
|
|
|
|
|
|
* plugins_service_order_pay_success_handle_end
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $params 钩子参数,含 business_data, user_id, business_ids(order_ids)
|
|
|
|
|
|
* @return bool
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function onOrderPaid($params = [])
|
|
|
|
|
|
{
|
|
|
|
|
|
$order_id = $params['business_id'] ?? ($params['business_ids'][0] ?? 0);
|
|
|
|
|
|
if (empty($order_id)) {
|
|
|
|
|
|
BaseService::log('onOrderPaid: empty order_id', $params, 'warning');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询订单
|
|
|
|
|
|
$order = \Db::name('Order')->find($order_id);
|
|
|
|
|
|
if (empty($order) || $order['pay_status'] != 1) {
|
|
|
|
|
|
BaseService::log('onOrderPaid: order not paid or not found', ['order_id' => $order_id], 'warning');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否为票务商品
|
|
|
|
|
|
if (!BaseService::isTicketGoods($order['goods_id'])) {
|
|
|
|
|
|
BaseService::log('onOrderPaid: not a ticket goods', ['order_id' => $order_id], 'info');
|
|
|
|
|
|
return true; // 不是票务商品,不报错
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询商品快照(规格信息)
|
|
|
|
|
|
$order_goods = \Db::name('OrderGoods')
|
|
|
|
|
|
->where('order_id', $order_id)
|
|
|
|
|
|
->select();
|
|
|
|
|
|
if (empty($order_goods)) {
|
|
|
|
|
|
BaseService::log('onOrderPaid: no order goods', ['order_id' => $order_id], 'error');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 逐个生成票(每个规格选项 = 一张票)
|
|
|
|
|
|
$count = 0;
|
|
|
|
|
|
foreach ($order_goods as $og) {
|
|
|
|
|
|
$ticket_id = self::issueTicket($order, $og);
|
|
|
|
|
|
if ($ticket_id > 0) {
|
|
|
|
|
|
$count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
BaseService::log('onOrderPaid: success', [
|
|
|
|
|
|
'order_id' => $order_id,
|
|
|
|
|
|
'tickets_issued' => $count,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return $count > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发放单张票
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $order 订单数据
|
|
|
|
|
|
* @param array $order_goods 订单商品数据(包含 spec_base_id)
|
|
|
|
|
|
* @return int 票ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function issueTicket($order, $order_goods)
|
|
|
|
|
|
{
|
2026-04-15 08:59:22 +00:00
|
|
|
|
// P0-1 幂等保护:同一订单+同一规格只发一张票
|
|
|
|
|
|
$existing = \Db::name(BaseService::table('tickets'))
|
|
|
|
|
|
->where('order_id', $order['id'])
|
|
|
|
|
|
->where('spec_base_id', $order_goods['spec_base_id'] ?? 0)
|
|
|
|
|
|
->find();
|
|
|
|
|
|
if (!empty($existing)) {
|
|
|
|
|
|
BaseService::log('issueTicket: idempotent_skip', [
|
|
|
|
|
|
'order_id' => $order['id'],
|
|
|
|
|
|
'spec_base_id'=> $order_goods['spec_base_id'] ?? 0,
|
|
|
|
|
|
], 'info');
|
|
|
|
|
|
return $existing['id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 05:09:44 +00:00
|
|
|
|
$ticket_code = BaseService::generateUuid();
|
|
|
|
|
|
|
|
|
|
|
|
// 构建 QR 数据
|
|
|
|
|
|
$qr_payload = [
|
|
|
|
|
|
'id' => 0, // 写入后再更新
|
|
|
|
|
|
'code' => $ticket_code,
|
|
|
|
|
|
'event' => $order['goods_id'],
|
|
|
|
|
|
'seat' => $order_goods['spec_name'] ?? '', // 规格名=座位信息
|
|
|
|
|
|
];
|
|
|
|
|
|
$qr_data = BaseService::encryptQrData($qr_payload);
|
|
|
|
|
|
|
|
|
|
|
|
// 观演人信息(从订单扩展字段读取,由购票页表单写入)
|
|
|
|
|
|
$extension_data = json_decode($order['extension_data'] ?? '{}', true);
|
|
|
|
|
|
$attendee = $extension_data['attendee'] ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
$now = BaseService::now();
|
|
|
|
|
|
|
|
|
|
|
|
$ticket_id = \Db::name(BaseService::table('tickets'))->insertGetId([
|
|
|
|
|
|
'order_id' => $order['id'],
|
|
|
|
|
|
'order_no' => $order['order_no'],
|
|
|
|
|
|
'goods_id' => $order['goods_id'],
|
|
|
|
|
|
'goods_snapshot' => json_encode([
|
|
|
|
|
|
'goods_name' => $order['goods_name'] ?? '',
|
|
|
|
|
|
'spec_name' => $order_goods['spec_name'] ?? '',
|
|
|
|
|
|
'price' => $order_goods['goods_price'] ?? 0,
|
|
|
|
|
|
], JSON_UNESCAPED_UNICODE),
|
|
|
|
|
|
'user_id' => $order['user_id'],
|
|
|
|
|
|
'ticket_code' => $ticket_code,
|
|
|
|
|
|
'qr_data' => $qr_data,
|
|
|
|
|
|
'seat_info' => $order_goods['spec_name'] ?? '',
|
|
|
|
|
|
'spec_base_id' => $order_goods['spec_base_id'] ?? 0,
|
|
|
|
|
|
'real_name' => $attendee['real_name'] ?? '',
|
|
|
|
|
|
'phone' => $attendee['phone'] ?? '',
|
|
|
|
|
|
'id_card' => $attendee['id_card'] ?? '',
|
|
|
|
|
|
'verify_status' => 0, // 0=未核销
|
|
|
|
|
|
'issued_at' => $now,
|
|
|
|
|
|
'created_at' => $now,
|
|
|
|
|
|
'updated_at' => $now,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 QR 数据中的 ticket_id
|
|
|
|
|
|
if ($ticket_id > 0) {
|
|
|
|
|
|
$qr_payload['id'] = $ticket_id;
|
|
|
|
|
|
$qr_data_updated = BaseService::encryptQrData($qr_payload);
|
|
|
|
|
|
\Db::name(BaseService::table('tickets'))
|
|
|
|
|
|
->where('id', $ticket_id)
|
|
|
|
|
|
->update(['qr_data' => $qr_data_updated]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $ticket_id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-15 06:03:00 +00:00
|
|
|
|
* 核销票(事务保护 + 悲观锁防并发)
|
2026-04-15 05:09:44 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @param string $ticket_code 票码
|
|
|
|
|
|
* @param int $verifier_id 核销员ID
|
|
|
|
|
|
* @return array [code, msg]
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function verifyTicket($ticket_code, $verifier_id)
|
|
|
|
|
|
{
|
2026-04-15 06:03:00 +00:00
|
|
|
|
try {
|
|
|
|
|
|
return \Db::transaction(function () use ($ticket_code, $verifier_id) {
|
|
|
|
|
|
// FOR UPDATE 悲观锁:防止并发核销同一张票
|
|
|
|
|
|
$ticket = \Db::name(BaseService::table('tickets'))
|
|
|
|
|
|
->where('ticket_code', $ticket_code)
|
|
|
|
|
|
->lock(true)
|
|
|
|
|
|
->find();
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($ticket)) {
|
|
|
|
|
|
return ['code' => -1, 'msg' => '票码不存在'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($ticket['verify_status'] == 1) {
|
|
|
|
|
|
return ['code' => -2, 'msg' => '该票已核销'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($ticket['verify_status'] == 2) {
|
|
|
|
|
|
return ['code' => -3, 'msg' => '该票已退款'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$now = BaseService::now();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新票状态
|
|
|
|
|
|
\Db::name(BaseService::table('tickets'))
|
|
|
|
|
|
->where('id', $ticket['id'])
|
|
|
|
|
|
->update([
|
|
|
|
|
|
'verify_status' => 1,
|
|
|
|
|
|
'verify_time' => $now,
|
|
|
|
|
|
'verifier_id' => $verifier_id,
|
|
|
|
|
|
'updated_at' => $now,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 写入核销记录
|
|
|
|
|
|
$verifier = \Db::name(BaseService::table('verifiers'))
|
|
|
|
|
|
->where('id', $verifier_id)
|
|
|
|
|
|
->find();
|
|
|
|
|
|
|
|
|
|
|
|
\Db::name(BaseService::table('verifications'))->insert([
|
|
|
|
|
|
'ticket_id' => $ticket['id'],
|
|
|
|
|
|
'ticket_code' => $ticket_code,
|
|
|
|
|
|
'verifier_id' => $verifier_id,
|
|
|
|
|
|
'verifier_name'=> $verifier['name'] ?? '',
|
|
|
|
|
|
'goods_id' => $ticket['goods_id'],
|
|
|
|
|
|
'created_at' => $now,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
BaseService::log('verifyTicket: success', [
|
|
|
|
|
|
'ticket_id' => $ticket['id'],
|
|
|
|
|
|
'verifier_id' => $verifier_id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-04-15 06:20:03 +00:00
|
|
|
|
// 审计日志(失败也记录,便于追溯异常)
|
|
|
|
|
|
AuditService::logVerify(
|
|
|
|
|
|
$ticket['id'],
|
|
|
|
|
|
$ticket_code,
|
|
|
|
|
|
$verifier_id,
|
|
|
|
|
|
$verifier['name'] ?? '',
|
|
|
|
|
|
'success',
|
|
|
|
|
|
0 // 原状态(核销前一定是 0)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-15 06:03:00 +00:00
|
|
|
|
return [
|
|
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'msg' => '核销成功',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'seat_info' => $ticket['seat_info'],
|
|
|
|
|
|
'real_name' => $ticket['real_name'],
|
|
|
|
|
|
'goods_name' => json_decode($ticket['goods_snapshot'] ?? '{}', true)['goods_name'] ?? '',
|
|
|
|
|
|
],
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
BaseService::log('verifyTicket: transaction_error', [
|
|
|
|
|
|
'ticket_code' => $ticket_code,
|
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
|
], 'error');
|
|
|
|
|
|
return ['code' => -999, 'msg' => '核销失败,请重试'];
|
2026-04-15 05:09:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取用户所有票
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function getUserTickets($user_id, $status = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
$where = ['user_id' => $user_id];
|
|
|
|
|
|
if ($status !== null) {
|
|
|
|
|
|
$where['verify_status'] = $status;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return \Db::name(BaseService::table('tickets'))
|
|
|
|
|
|
->where($where)
|
|
|
|
|
|
->order('created_at', 'desc')
|
|
|
|
|
|
->select();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成 QR 码图片 URL
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $ticket_code
|
|
|
|
|
|
* @return string QR码图片URL
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function getQrCodeUrl($ticket_code)
|
|
|
|
|
|
{
|
|
|
|
|
|
$content = base64_encode(json_encode([
|
|
|
|
|
|
'type' => 'vr_ticket',
|
|
|
|
|
|
'code' => $ticket_code,
|
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
|
|
return ROOT_URL . '?s=index/qrcode/index&content=' . urlencode($content) . '&size=8&level=H';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|