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

309 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 场馆/座位模板硬删除问题评估报告
**项目**: 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)
```php
// 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
}
```
**影响**:
- `$seatTemplate``null`,前端 `ticket_detail.html` 无法渲染座位图
- 页面仍能显示PHP Warning 不中断),但座位图区域空白
#### 1.2 保存时 (AdminGoodsSaveHandle约 line 70-90)
```php
// 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 前端票务详情页显示
```php
// 返回结构
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)
```php
// 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)
```php
// 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**: 逻辑外键约束(推荐)
```sql
-- 创建 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**: 硬删除前检查
```php
// 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` 中,作为备份数据源。
```php
// 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`
```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`
```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()` |