vr-shopxo-plugin/reports/venue-hard-delete-evaluatio...

9.7 KiB
Raw Blame History

场馆/座位模板硬删除问题评估报告

项目: VR票务插件 (vr-shopxo-plugin)
评估人: Architect + PM
日期: 2026-04-20
状态: P0 需立即处理


摘要

当前系统对 vr_seat_templates 实施软删除 (status=0),若引入硬删除会导致以下问题:

  • 商品编辑时模板读取失败 → seatTemplate = null
  • 商品保存时 json_decode(null) 报错 → 500错误
  • 前端票务详情页无法显示座位图

核心问题: AdminGoodsSaveHandle 第60-110行在重建 template_snapshot 时缺少空值检测,硬删除后访问已删模板会触发 Fatal Error。


Q1 影响评估

场景还原

当模板 ID=5 被硬删除,商品 A 仍关联 template_id=5

1.1 读取时 (GetGoodsViewData约 line 350)

// SeatSkuService.php:358-365
$seatTemplate = \think\facade\Db::name(self::table('seat_templates'))
    ->where('id', $templateId)
    ->find();

// 如果模板不存在(硬删除)→ $seatTemplate = null
// 后续对 $seatTemplate['seat_map'] 的直接访问会报 Notice 或 Warning
if (!empty($seatTemplate['seat_map'])) {
    $decoded = json_decode($seatTemplate['seat_map'], true); // Warning: null passed
}

影响:

  • $seatTemplatenull,前端 ticket_detail.html 无法渲染座位图
  • 页面仍能显示PHP Warning 不中断),但座位图区域空白

1.2 保存时 (AdminGoodsSaveHandle约 line 70-90)

// AdminGoodsSaveHandle.php:70-85
$template = Db::name('vr_seat_templates')->find($templateId); // null
$seatMap  = json_decode($template['seat_map'] ?? '{}', true); // FATAL: Cannot access null
$allRooms = $seatMap['rooms'] ?? []; // Warning: null

影响:

  • P0 - 触发 PHP Fatal Error 导致保存失败
  • 错误信息: Error: Call to a member function on null
  • 商品无法保存/更新

1.3 前端票务详情页显示

// 返回结构
return [
    'vr_seat_template' => $seatTemplate ?: null,  // null → 页面无座位图
    'goods_spec_data'  => $goodsSpecData,
    'goods_config'    => $config,
];

影响:

  • 前端票务详情页座位图区域空白
  • 用户无法选座(但不影响已购票的 goods_snapshot

Q2 修复方案

方案对比

方案 优点 缺点 推荐度
A: GetGoodsViewData 加 fallback 改动小,不影响保存流程 治标不治本
B: AdminGoodsSaveHandle 加检测+提示 可阻止脏数据写入 需要改两个地方
C: 删除模板时级联处理 彻底解决孤立引用 改动大,破坏软删除语义

推荐: 方案 B + 方案 A 组合

Step 1: GetGoodsViewData 加 fallback (方案 A)

// SeatSkuService.php:365新增
$seatTemplate = \think\facade\Db::name(self::table('seat_templates'))
    ->where('id', $templateId)
    ->find();

// ▼ 新增: 模板不存在时,检查 template_snapshot
if (empty($seatTemplate) && !empty($config['template_snapshot'])) {
    // 使用 snapshot 恢复模板数据
    $seatTemplate = [
        'seat_map' => json_encode($config['template_snapshot'], JSON_UNESCAPED_UNICODE),
    ];
}

Step 2: AdminGoodsSaveHandle 加检测 (方案 B)

// AdminGoodsSaveHandle.php:68-72新增
$templateId    = intval($config['template_id'] ?? 0);
$selectedRooms = $config['selected_rooms'] ?? [];

// ▼ 新增: 检测模板是否存在
$template = Db::name('vr_seat_templates')->find($templateId);
$templateExists = !empty($template);

// 条件: snapshot 为空,或者前端有 selected_rooms
if ($templateId > 0 && (!empty($selectedRooms) || empty($config['template_snapshot']) || empty($config['template_snapshot']['rooms']))) {
    // 如果模板已删除且没有 snapshot拒绝保存
    if (!$templateExists && empty($config['template_snapshot'])) {
        return ['code' => -401, 'msg' => '座位模板已被删除,请重新选择模板'];
    }
    
    // ▼ 新增: 模板不存在但有 snapshot 时,从 snapshot 恢复
    if (!$templateExists && !empty($config['template_snapshot'])) {
        $seatMap = ['venue' => $config['template_snapshot']['venue'] ?? [], 'rooms' => $config['template_snapshot']['rooms'] ?? []];
    } else {
        $seatMap = json_decode($template['seat_map'] ?? '{}', true);
    }
    // ...
}

Q3 真删除功能设计

UI/UX 建议

按钮 当前文字 建议文字 备注
软删除 删除 禁用 现有行为: status→0
硬删除 - 删除 需二次确认

警告弹窗设计:

┌─────────────────────────────────┐
│  确定要删除此模板吗?           │
├─────────────────────────────────┤
│  ⚠️  此操作不可恢复            │
│                                 │
│  □  同时解除商品关联(推荐)   │
│  □  强制删除(忽略关联)        │
│                                 │
│    [取消]        [确定删除]    │
└─────────────────────────────────┘

数据库操作

方案 1: 逻辑外键约束(推荐)

-- 创建 FK但不启用 ON DELETE CASCADE
ALTER TABLE vr_goods_config 
ADD CONSTRAINT fk_template_soft 
FOREIGN KEY (template_id) 
REFERENCES vr_seat_templates(id) 
ON DELETE NO ACTION;

-- 软删除时不清除外键,只是查不到
-- 需要显示检查关联商品,在应用层处理

方案 2: 硬删除前检查

// Admin.php: SeatTemplateDelete 新增参数
public function SeatTemplateDelete()
{
    $id = input('id', 0, 'intval');
    $force = input('force', 0, 'intval'); // 强制删除 flag
    
    if (!$force) {
        // 检查是否有商品关联
        $goods = Db::name('goods')
            ->where('vr_goods_config', 'like', '%"template_id":' . $id . '%')
            ->find();
        
        if (!empty($goods)) {
            return DataReturn('该模板有关联商品,无法删除', -402);
        }
    }
    
    // 硬删除
    Db::name('vr_seat_templates')->where('id', $id)->delete(true);
}

template_snapshot 处理

原则: 删除模板时,template_snapshot 保留在 vr_goods_config 中,作为备份数据源。

// AdminGoodsSaveHandle.php snapshot 恢复逻辑已覆盖此场景
// 删除模板不影响已有商品的 snapshot

Q4 优先级定义

P0必须修复立即

问题 描述 修复位置
AdminGoodsSaveHandle 空指针 硬删除后保存商品 Fatal Error AdminGoodsSaveHandle.php:68-90
GetGoodsViewData 空值 编辑时模板不存在导致 Warning SeatSkuService.php:358-365

P1下一迭代

问题 描述 修复位置
模板删除检查 删除模板前检查商品关联 Admin.php: SeatTemplateDelete
UI 改名为"禁用" 软删除按钮文案改为"禁用" admin/view/seat_template/*.html

P2后续优化

问题 描述 修复位置
真删除功能 硬删除 API + 二次确认弹窗 Admin.php: SeatTemplateDelete + View
FK 约束增强 考虑添加数据库外键约束 SQL migration

修复步骤

Step 1: 紧急修复 (P0)

文件: shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php

// 约 line 68-72 修改
$templateId    = intval($config['template_id'] ?? 0);
$selectedRooms = $config['selected_rooms'] ?? [];

// ▼ 新增: 检测模板是否存在
$template = Db::name('vr_seat_templates')->find($templateId);
$templateExists = !empty($template);

// 条件: snapshot 为空,或者前端有 selected_rooms
if ($templateId > 0 && (!empty($selectedRooms) || empty($config['template_snapshot']) || empty($config['template_snapshot']['rooms']))) {
    // ▼ 新增: 模板不存在且没有 snapshot拒绝保存
    if (!$templateExists && empty($config['template_snapshot'])) {
        return ['code' => -401, 'msg' => '座位模板已被删除,请重新选择模板'];
    }
    
    // ▼ 新增: 模板不存在但有 snapshot 时,从 snapshot 恢复
    $seatMap = !$templateExists && !empty($config['template_snapshot']) 
        ? ['venue' => $config['template_snapshot']['venue'] ?? [], 'rooms' => $config['template_snapshot']['rooms'] ?? []]
        : json_decode($template['seat_map'] ?? '{}', true);
    // ... 后续逻辑不变
}

文件: shopxo/app/plugins/vr_ticket/service/SeatSkuService.php

// 约 line 358-365 修改
$seatTemplate = \think\facade\Db::name(self::table('seat_templates'))
    ->where('id', $templateId)
    ->find();

// ▼ 新增: 模板不存在时,检查 template_snapshot
if (empty($seatTemplate) && !empty($config['template_snapshot'])) {
    $seatTemplate = [
        'seat_map' => json_encode($config['template_snapshot'], JSON_UNESCAPED_UNICODE),
    ];
}

Step 2: UX 优化 (P1)

  • 修改按钮文案: "删除" → "禁用"
  • 新增硬删除确认弹窗

Step 3: 完整功能 (P2)

  • 实现硬删除 API
  • 添加商品关联检查

风险说明

当前系统不存在真正的硬删除,所有删除都是软删除。评估基于计划引入硬删除功能的假设。

如不实施硬删除,则 Q1 不会触发,仅需 Q2 方案 A 作为防御性编程。


附录: 代码路径汇总

文件 行号 函数
service/SeatSkuService.php 350-420 GetGoodsViewData()
hook/AdminGoodsSaveHandle.php 60-110 重建 template_snapshot
admin/Admin.php 227-255 SeatTemplateDelete()
admin/Admin.php 803-830 VenueDelete()