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

351 lines
12 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
- 添加商品关联检查
---
## 决策记录
### 方案选定:方案 C用户提出— 置空 + 自清理
**决策日期**2026-04-20
**核心思路**(用户提出):
> 模板如果删除说明用户不要了,否则他就应该设置禁用。既然删除,等商品卖完继续上架,不存在的配置本来就应该同步不要了。
**用户意图**:删除模板 = 用户主动放弃该模板 → 商品的 template_snapshot 也应一并清空,让商品下次保存时整块 config 干净地失效,而不是保留旧 snapshot 导致"有 snapshot 但无 template"的不一致状态。
**最终方案逻辑**
1. `GetGoodsViewData()` 检测到模板不存在 → 将 `template_id``template_snapshot` 同时置 null → 写回 DB
2. 前端打开编辑 → 选单为空(因为 template_id=null 对应不上任何模板)
3. 用户保存(无 template_id`AdminGoodsSaveHandle` 的 snapshot 重建条件 `$templateId > 0` 不满足 → 跳过重建 → config 块无 snapshot
4. 商品彻底脱钩,不存在任何指向已删模板的数据
**警告文案**(删除确认弹窗):
> 删除记录不会导致已上架商品内容变动。若需要同步场馆信息到已发布商品,请编辑对应商品并保存。
### 最终实现
**文件 1**`service/SeatSkuService.php` - `GetGoodsViewData()`
- 模板不存在时,`template_id = null` + `template_snapshot = null`
- 同步写回 `vr_goods_config` 到 DB
- 返回 `null` 模板,前端座位图区域空白
**文件 2**`hook/AdminGoodsSaveHandle.php` - 重建 snapshot 逻辑
- `Db::find($templateId)` 返回 null 时 → `continue`
- 不执行后续 `json_decode($template['seat_map'])`(避免 Fatal Error
- BatchGenerate 条件 `$templateId > 0` 不满足 → 跳过 SKU 生成
### 与方案 A+B 的对比
| | 方案 A+B | 方案 C最终 |
|---|---|---|
| 模板不存在时 | fallback 到 snapshot | 置空 template_id + snapshot |
| 用户感知 | 旧数据仍可见 | 选单为空,需重新选择 |
| 数据一致性 | 混合状态(无 template_id 但有 snapshot| 干净清空 |
| 复杂度 | 两处改动 | 一处读+一处写 |
| 符合用户意图 | 中等 | ✅ 完全一致 |
### 风险说明
- 删除模板前已售出的票不受影响(`goods_snapshot` 是购买时快照)
- 正在编辑中的商品,下次保存会自动清空配置(符合"不要了"的语义)
- 已上架商品(未重新编辑)保持原状态 → 下次编辑时才发现模板缺失 → 也按上述逻辑处理
---
## 附录: 代码路径汇总
| 文件 | 行号 | 函数 |
|------|------|------|
| `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()` |