fix(P0): P0-1 idempotent ticket issuance, P0-3 XSS, P0-4 QR secret exception
P0-1: issueTicket() now checks for existing tickets by (order_id, spec_base_id)
before inserting. Prevents duplicate tickets on HTTP retry/multi-instance.
P0-3: Removed |raw from simple_desc and content in ticket_detail.html.
Prevents stored XSS via malicious admin content injection.
P0-4: getQrSecret() now throws exception if VR_TICKET_QR_SECRET is unset,
instead of falling back to insecure default key.
refactor/vr-ticket-20260416
parent
9171046435
commit
098bcfe780
|
|
@ -97,13 +97,11 @@ class BaseService
|
||||||
*/
|
*/
|
||||||
private static function getQrSecret()
|
private static function getQrSecret()
|
||||||
{
|
{
|
||||||
// 优先从环境变量读取
|
|
||||||
$secret = env('VR_TICKET_QR_SECRET', '');
|
$secret = env('VR_TICKET_QR_SECRET', '');
|
||||||
if (!empty($secret)) {
|
if (empty($secret)) {
|
||||||
return $secret;
|
throw new \Exception('[vr_ticket] VR_TICKET_QR_SECRET 环境变量未配置,QR加密密钥不能为空。请在.env中设置VR_TICKET_QR_SECRET=<随机64字符字符串>');
|
||||||
}
|
}
|
||||||
// 回退:使用 ShopXO 应用密钥
|
return $secret;
|
||||||
return config('shopxo.app_key', 'shopxo_default_secret_change_me');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,19 @@ class TicketService extends BaseService
|
||||||
*/
|
*/
|
||||||
public static function issueTicket($order, $order_goods)
|
public static function issueTicket($order, $order_goods)
|
||||||
{
|
{
|
||||||
|
// 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'];
|
||||||
|
}
|
||||||
|
|
||||||
$ticket_code = BaseService::generateUuid();
|
$ticket_code = BaseService::generateUuid();
|
||||||
|
|
||||||
// 构建 QR 数据
|
// 构建 QR 数据
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
<!-- 商品头部 -->
|
<!-- 商品头部 -->
|
||||||
<div class="vr-ticket-header">
|
<div class="vr-ticket-header">
|
||||||
<div class="vr-event-title">{$goods.title|default='VR演唱会'}</div>
|
<div class="vr-event-title">{$goods.title|default='VR演唱会'}</div>
|
||||||
<div class="vr-event-subtitle">{$goods.simple_desc|default=''|raw}</div>
|
<div class="vr-event-subtitle">{$goods.simple_desc|default=''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 场次选择 -->
|
<!-- 场次选择 -->
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
{if !empty($goods.content)}
|
{if !empty($goods.content)}
|
||||||
<div class="vr-seat-section">
|
<div class="vr-seat-section">
|
||||||
<div class="vr-section-title">演出详情</div>
|
<div class="vr-section-title">演出详情</div>
|
||||||
<div class="goods-detail-content">{$goods.content|raw}</div>
|
<div class="goods-detail-content">{$goods.content}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue