vr-shopxo-plugin/reviews/council-ghost-spec-Frontend...

255 lines
9.8 KiB
Markdown
Raw Permalink 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.

# FrontendDev 调研报告:幽灵 spec 问题
> 日期2026-04-20 | Agentcouncil/FrontendDev
---
## 1. ticket_detail.html 的前端规格项构建
### 1.1 页面性质确认
`ticket_detail.html` 是**客户前端购票页面**(用于 C 端用户选座下单),**不是**后台商品编辑页面。后台编辑商品时出现的「规格不允许重复」错误发生在 ShopXO 标准后台,其触发点在 `GoodsService.php:1859/1889/1925`
前端购票页面的数据来源:
| PHP 变量 | 来源SeatSkuService | 用途 |
|----------|----------------------|------|
| `$vr_seat_template` | `GetGoodsViewData()` | `seat_map`、`spec_base_id_map` |
| `$goods_spec_data` | `GetGoodsViewData()` | 场次session列表 |
前端 JS 接收这些数据:
```
ticket_detail.html:186-187
seatMap: <?php echo json_encode($vr_seat_template['seat_map'] ?? []); ?>,
specBaseIdMap: <?php echo json_encode($vr_seat_template['spec_base_id_map'] ?? []); ?>,
```
前端规格项(场次)构建逻辑(`renderSessions()`, ticket_detail.html:202-213
```javascript
var specData = <?php echo json_encode($goods_spec_data ?? []); ?> || [];
// specData 格式: [{spec_id: 2001, spec_name: "08:00-23:59", price: 100}]
// 渲染为可点击的场次卡片
```
**结论**`ticket_detail.html` 本身不构建 ShopXO 规格spec表格其规格项仅为场次选择器。真正触发「规格不允许重复」的是 ShopXO 后台商品编辑页的 `GoodsService.php`
---
## 2. 模板不存在时前端对 template_snapshot 和 spec_base_id_map 的处理
### 2.1 后端 fallback 行为SeatSkuService.php
关键函数:`GetGoodsViewData()` (`SeatSkuService.php:358-464`)
**模板不存在时的 fallback硬删除场景**
```php
// SeatSkuService.php:383-393
if (empty($seatTemplate)) {
$config['template_id'] = null;
$config['template_snapshot'] = null;
Db::name('Goods')->where('id', $goodsId)->update([
'vr_goods_config' => json_encode([$config], ...),
]);
return [
'vr_seat_template' => null,
'goods_spec_data' => [],
'goods_config' => $config,
];
}
```
**执行效果**
1. `template_id` 被置为 `null`(写入 DB
2. `template_snapshot` 被置为 `null`(写入 DB
3. 返回给前端:`vr_seat_template = null`、`goods_spec_data = []`
**前端接收到的数据**
```javascript
seatMap: {} // 空对象
specBaseIdMap: {} // 空对象
goods_spec_data: [] // 空数组
```
**前端渲染结果**
- `renderSessions()``sessionGrid` 内为 `goods_spec_data.length === 0`显示提示「该商品暂无场次信息」ticket_detail.html:133
- `renderSeatMap()``seatMap.map` 为空,座位图区域显示「座位图加载失败」
- 整个座位选择区域 UI 为空/失败状态
### 2.2 根因分析
**模板不存在时,前端的 fallback 行为是正确的**——前端展示空白购票页,用户无法选座。这符合"场馆已删除,无法购票"的业务预期。
真正的问题不在 `ticket_detail.html`(前端),而在:
1. 后台商品编辑页ShopXO admin——保存时 `AdminGoodsSaveHandle` 如何处理 `template_id=null` 的情况
2. `vr_goods_config` 的持久化清理——硬删除后 `vr_goods_config` 中的 config 块是否被正确清理
---
## 3. loadSoldSeats() 函数实现情况
**状态:未实现(仅有 TODO 注释)**
```
ticket_detail.html:375-383
loadSoldSeats: function() {
// TODO: 从后端加载已售座位
// $.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', {
// goods_id: this.goodsId,
// spec_base_id: this.sessionSpecId
// }, function(res) {
// // 标记已售座位
// });
},
```
**影响**
- `soldSeats: {}` 永远为空对象ticket_detail.html:189
- `renderSeatMap()` 渲染座位时,无法从 `soldSeats` 读取已售标记
- 已售座位只能通过 `.sold` class由 PHP 渲染)或 `soldSeats` 字典来标记,但两者都未生效
- 结果:前端无法区分已售/可选座位——用户可能选中一个已售座位,提交后才发现无法购买
**严重程度**P2功能缺陷不影响「规格不允许重复」错误。
---
## 4. 编辑模式下前端对已删除场馆旧规格的处理
### 4.1 当前行为
当商品的 `vr_goods_config``template_id` 指向的场馆已被硬删除:
1. `GetGoodsViewData()` 检测到模板不存在 → `template_id=null`、`template_snapshot=null` → 写入 DB
2. 前端收到 `vr_seat_template=null`、`goods_spec_data=[]`
3. `ticket_detail.html` 渲染空白购票页(无场次、无座位图)
4. **前端没有特殊逻辑处理幽灵 spec**——因为后端已经清理了 `template_id``template_snapshot`
### 4.2 问题点
**`ticket_detail.html` 是前端购票页,不是编辑页**。商品编辑(后台)由 ShopXO 标准后台处理VR 插件通过钩子介入。
幽灵 spec 的真正风险在于 `AdminGoodsSaveHandle` 的保存逻辑:
- `AdminGoodsSaveHandle.php:383-394`(硬删除 fallback当模板不存在时`continue` 跳过 snapshot 重建,**但 config 块本身未被移除**
- 如果 `vr_goods_config` 包含多个 config 块(如多场馆商品),硬删除场馆后该 config 块残存
- 下次编辑时,该 config 块仍被读取,若前端重新选择了场馆,可能导致 spec 重复
---
## 5. 前端根因分析
### 5.1 「规格不允许重复」错误的真正触发点
该错误**不在 `ticket_detail.html`**,而在 ShopXO 后台商品编辑流程的 `GoodsService.php:1859/1889/1925`
触发条件:
1. 用户在 ShopXO 后台编辑商品时,手动填写/复制了重复的规格值
2. 表单提交到 `GoodsService::GoodsSave()` → spec 验证逻辑检查 `specifications_value_*` 参数
3. 发现有重复值 → 返回「规格不允许重复」错误
### 5.2 与 VR 插件的关联
`AdminGoodsSaveHandle` 运行时(`plugins_service_goods_save_thing_end`),它会:
1. 清空 `GoodsSpecType`、`GoodsSpecBase`、`GoodsSpecValue`AdminGoodsSaveHandle.php:152-155
2.`template_id > 0` 的 config 块执行 `BatchGenerate`
如果 `template_id``null`(硬删除后),`BatchGenerate` 跳过,但 `vr_goods_config` 中的 config 块仍然残存。**此时商品 spec 表为空**,不会出现「规格不允许重复」错误。
但如果用户在前端ShopXO 后台编辑页操作时ShopXO 的原生规格表单被填充了旧的 VR 规格数据,这些数据可能在保存时被 ShopXO 的原生规格逻辑验证并触发重复错误。
---
## 6. 修复方案
### 6.1 前端修复ticket_detail.html
**loadSoldSeats() 建议实现**
```javascript
loadSoldSeats: function() {
if (!this.goodsId || !this.sessionSpecId) return;
var self = this;
$.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', {
goods_id: this.goodsId,
spec_base_id: this.sessionSpecId
}, function(res) {
if (res.code === 0 && res.data) {
self.soldSeats = res.data; // {row_col: true, ...}
self.markSoldSeats();
}
});
},
markSoldSeats: function() {
var self = this;
document.querySelectorAll('.vr-seat').forEach(function(el) {
var key = el.dataset.rowLabel + '_' + el.dataset.colNum;
if (self.soldSeats[key]) {
el.classList.add('sold');
}
});
},
```
### 6.2 后端修复(建议 BackendArchitect 评估)
当模板被硬删除后,`AdminGoodsSaveHandle` 应清理整个 config 块:
```php
// AdminGoodsSaveHandle.php:77-90 改进
if (empty($template)) {
// 模板不存在时,移除整个 config 块(避免残存)
unset($configs[$i]);
continue;
}
$configs = array_values($configs); // 重排索引
```
或在 `SeatSkuService::GetGoodsViewData()` 中持久化清理:
```php
// SeatSkuService.php:383-393 改进
if (empty($seatTemplate)) {
// 模板不存在时,清除整个 config 块,而非仅置 null
$vrGoodsConfig = json_decode($goods['vr_goods_config'] ?? '', true);
unset($vrGoodsConfig[0]);
$newConfig = array_values($vrGoodsConfig);
Db::name('Goods')->where('id', $goodsId)->update([
'vr_goods_config' => empty($newConfig) ? '' : json_encode($newConfig, ...),
]);
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}
```
---
## 7. 总结
| 问题 | 位置 | 严重度 | 说明 |
|------|------|--------|------|
| loadSoldSeats() 未实现 | ticket_detail.html:375 | P2 | 已售座位无法标记 |
| 模板不存在时 fallback 正确 | SeatSkuService.php:383 | — | 后端已正确清理 template_id |
| 「规格不允许重复」不在前端触发 | GoodsService.php:1859 | — | 触发点在 ShopXO 后台服务层 |
| config 块残留 | AdminGoodsSaveHandle.php | P2 | 硬删除后 config 块未移除 |
| spec_base_id_map 不影响前端 | ticket_detail.html:417 | P3 | 前端通过 seatKey 查找,未使用 map |
---
## 8. 文件路径索引
| 文件 | 行号 | 关键内容 |
|------|------|---------|
| `SeatSkuService.php` | 358-464 | `GetGoodsViewData()`,模板不存在 fallback |
| `SeatSkuService.php` | 383-394 | 模板不存在时置 null 并更新 DB |
| `AdminGoodsSaveHandle.php` | 77-145 | config 块遍历和 snapshot 重建逻辑 |
| `AdminGoodsSaveHandle.php` | 152-155 | 清空原生 spec 表 |
| `AdminGoodsSaveHandle.php` | 158-173 | BatchGenerate 循环(跳过 template_id=0|
| `ticket_detail.html` | 186-189 | 前端 JS 接收 seatMap/specBaseIdMap |
| `ticket_detail.html` | 202-213 | `renderSessions()` 场次渲染 |
| `ticket_detail.html` | 375-383 | `loadSoldSeats()` TODO未实现|
| `ticket_detail.html` | 417 | specBaseIdMap 查找(仅 Plan A 提交用)|
| `GoodsService.php` | 1859 | 规格值列重复检测 |
| `GoodsService.php` | 1889 | 规格值重复检测 |
| `GoodsService.php` | 1925 | 规格名称重复检测 |