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()
|
||||
{
|
||||
// 优先从环境变量读取
|
||||
$secret = env('VR_TICKET_QR_SECRET', '');
|
||||
if (!empty($secret)) {
|
||||
return $secret;
|
||||
if (empty($secret)) {
|
||||
throw new \Exception('[vr_ticket] VR_TICKET_QR_SECRET 环境变量未配置,QR加密密钥不能为空。请在.env中设置VR_TICKET_QR_SECRET=<随机64字符字符串>');
|
||||
}
|
||||
// 回退:使用 ShopXO 应用密钥
|
||||
return config('shopxo.app_key', 'shopxo_default_secret_change_me');
|
||||
return $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -76,6 +76,19 @@ class TicketService extends BaseService
|
|||
*/
|
||||
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();
|
||||
|
||||
// 构建 QR 数据
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@
|
|||
<!-- 商品头部 -->
|
||||
<div class="vr-ticket-header">
|
||||
<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>
|
||||
|
||||
<!-- 场次选择 -->
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
{if !empty($goods.content)}
|
||||
<div class="vr-seat-section">
|
||||
<div class="vr-section-title">演出详情</div>
|
||||
<div class="goods-detail-content">{$goods.content|raw}</div>
|
||||
<div class="goods-detail-content">{$goods.content}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue