vr-shopxo-plugin/docs/EXPERIENCES.md

298 lines
9.1 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.

# VR票务插件 — 踩坑经验文档
> 本文档源自 2026-04-16 一整夜的重构调试,汇集了所有关键教训。
> **任何接手本项目的 agent请先阅读本文档。**
>
> 原始日志:`refactoring_log_vrticket_2026.md`4644行
> 提炼源:`refactoring_learnings.md`
---
## 🔴 P0 — 致命陷阱(必读)
### 1. `public/footer` 缺失 → 无限加载(最反直觉)
**现象**:列表页秒开,点击"新建/编辑"后页面永远转圈。
**假线索**后端死循环、数据库慢查询、unpkg.com CDN 阻断。
**真实根因**`save.html` 漏掉了 `{{:ModuleInclude('public/footer')}}`。AmazeUI 后台在页面跳转时显示全屏 Loading Spinner关闭动画的 JS 逻辑在 `footer` 的库文件里。没有 `footer` → Spinner 永不消失。
```html
<!-- ❌ 错误:漏 footer -->
{{:ModuleInclude('public/header')}}
<div>页面内容</div>
<!-- ✅ 正确:必须成对 -->
{{:ModuleInclude('public/header')}}
<div>页面内容</div>
{{:ModuleInclude('public/footer')}}
```
**教训**:无限加载 ≠ 后端死循环。AmazeUI Loading Spinner 遮罩是更常见的原因,优先检查 header/footer 是否完整。
---
### 2. Vue 3 `[[ ]]` 插值禁止用于 `<textarea>`
**现象**Base64 大字符串场景下页面完全无响应(浏览器卡死)。
**根因**`<textarea>[[ compiledJsonRaw ]]</textarea>` 使用双花括号插值绑定 Text NodeVue 3 在大数据动态赋值时触发虚拟 DOM 补丁机制无限死循环。
```html
<!-- ❌ 错误 -->
<textarea>[[ compiledJsonRaw ]]</textarea>
<!-- ✅ 正确:用隐藏 input -->
<input type="hidden" name="seat_map_raw" :value="compiledJsonRaw" />
```
**教训**Vue 3 插值语法 `[[ ]]` 只用于文本节点,禁止用于 `<textarea>` 的 value 属性。
---
### 3. 字段名不能猜,必须查源码
**现象**`is_delete_time` 报错,第一轮凭经验改字段名后仍然报错。
**根因**GoodsCategory 表根本没有软删除字段ShopXO 的软删除用的是 `is_enable` 而非 `is_delete_time`)。
```php
// ❌ 错误:凭经验猜字段名
if (!empty($category['is_delete_time']))
// ✅ 正确:查 GoodsCategoryService.php 源码确认字段
if (!empty($category['is_enable']))
```
**教训**ShopXO 部分表没有软删除字段。改字段名之前必须查对应 Service 层源码或实际表结构。
---
## 🟡 P1 — 严重问题
### 4. 插件视图路径:必须用 `../../../plugins/插件名/view/...`
ShopXO 插件控制器继承 `app\admin\controller\Common` 后,模板引擎默认去找 `app/admin/view/default/` 而非插件目录。
```php
// ❌ 错误:引擎截断路径
return MyView('venue/list');
// ✅ 正确:跨模块绝对路径
return MyView('../../../plugins/vr_ticket/view/venue/list');
```
**教训**:插件视图必须放在插件根目录的 `view/` 下(不是 `admin/view/`),且调用时加 `../../../plugins/插件名/view/...` 前缀。
---
### 5. Hook.php 返回值必须完整
ShopXO 的菜单渲染引擎要求 Hook 返回数组包含 `id`、`url`、`name`、`is_show` 完整字段。缺失任何一项都会导致侧边栏报错(`Undefined array key url` 等)。
```php
// ❌ 错误:缺少字段
return [
'title' => '场馆管理',
'control' => 'Venue',
'action' => 'list',
];
// ✅ 正确:完整字段
return [
'id' => 'venue-list',
'name' => '场馆管理',
'url' => MyUrl('vr_ticket/admin/venue-list'),
'is_show' => 1,
'control' => 'Venue',
'action' => 'list',
];
```
---
### 6. CDN 国内阻断:`unpkg.com` 不可用
**现象**Vue 3 编辑器完全失效,控制台有 `[Intervention] Slow network is detected`
**根因**`unpkg.com` 在中国大陆常发生静默阻断挂起。
```html
<!-- ❌ 错误:国内阻断 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- ✅ 正确 -->
<script src="https://cdn.staticfile.net/vue/3.x.x/vue.global.prod.js"></script>
```
**教训**:国内项目 CDN 必须用 `cdn.staticfile.net``cdn.bootcdn.net`,禁止 `unpkg.com`/`cdnjs.cloudflare.com`。
---
### 7. PHP 注释块污染:未闭合 `/*` 导致语法错误
**现象**:整个 Admin 控制器报 `syntax error, unexpected token "public"`
**根因**:调试期间遗留的未闭合 `/*` 注释块吞噬了后面的方法定义。
```php
/* 这是调试代码...
// 忘记闭合public 关键字被吞掉
public function index() { ... }
```
**教训**:隔离测试时清理调试代码;用 `php -l` 做语法检查。
---
### 8. ShopXO 路由 → `PluginsAdminUrl()` 而非硬编码
```php
// ❌ 错误:硬编码 URL
$url = '/adminwatekc.php?s=Plugins/VrTicket/Admin/index';
// ✅ 正确
$url = PluginsAdminUrl('vr_ticket', 'admin', 'index');
```
---
### 9. Admin.php `initialize()` 每次请求执行 `SHOW TABLES`
**现象**`save.html` 等页面加载极其缓慢。
**根因**Admin.php 的 `initialize()` 构造中每次请求都在执行完整的 `SHOW TABLES` 和重建引用脚本,巨大 I/O 负担。
**解法**引入基于时间跨度的缓存锁Cache Lock将高昂的表检查操作降低至 1 小时 1 次。
```php
protected function initialize()
{
parent::initialize();
$lockKey = 'vr_ticket_init_lock';
$cache = cache($lockKey);
if ($cache === false) {
// 执行表检查和初始化
$this->checkTables();
cache($lockKey, 1, 3600); // 锁1小时
}
}
```
---
### 10. ThinkPHP 6 分页:`->render()` 而非 `->toArray()` 后取 `page`
```php
// ❌ 错误
$list = $model->paginate(10)->toArray();
return $list['page']; // undefined
// ✅ 正确
$list = $model->paginate(10);
return $list->render();
```
---
## 🟢 P2 — 重要经验
### 11. ShopXO 插件目录结构(正确模式)
```
vr_ticket/
├── Admin.php ← 根目录,继承 think\Controller不是 admin/controller/
├── Hook.php ← 根目录
├── config.json
├── service/
│ └── BaseService.php
└── view/ ← 不是 admin/view/,是根目录 view/
└── venue/
├── list.html
└── save.html
```
**教训**:参考 `freightfee`/`answers` 插件结构,不要用 `admin/controller/` 子目录模式(会导致 strtolower+ucfirst 类名不匹配)。
---
### 12. JSON 传给 JS用 `<script type="text/json">` + `textContent`
```html
<!-- ❌ 错误:模板插值,含特殊字符时 JS 解析失败 -->
<script>var data = {{$seat_map | json_encode | raw}};</script>
<!-- ✅ 正确:隔绝特殊字符 -->
<script type="text/json" id="seat-map-data">{{$seat_map | json_encode | raw}}</script>
<script>var data = JSON.parse(document.getElementById('seat-map-data').textContent);</script>
```
---
### 13. AmazeUI 必用类名速查
| 功能 | AmazeUI 类 |
|------|-----------|
| 表格 | `am-table am-table-striped am-table-hover am-text-middle` |
| 按钮 | `am-btn am-btn-default` / `-primary` / `-danger` + `am-btn-xs am-radius` |
| 表单验证 | `class="am-form form-validation"`(没有这个 class 不走 AJAX |
| 字段校验提示 | `data-validation-message` 属性 |
| 徽章 | `am-badge am-badge-success` / `-danger` |
| 搜索栏 | `am-input-group am-input-group-sm am-fl so` |
| 分页 | `{{$page\|raw}}` |
| 图标 | `am-icon-plus` / `am-icon-edit` / `am-icon-trash-o` |
---
### 14. URL 截断Base64 编码兜底
ShopXO 框架内部有多层正则扫描"净化"资源路径CDN URL 会被截断。
```javascript
// 前端:提交前 Base64 编码
formData.seat_map = btoa(unescape(encodeURIComponent(jsonString)));
// 后端:解码还原
$seatMap = json_decode(base64_decode($_POST['seat_map']), true);
```
---
### 15. 搜索字段 = 列表主标题(字段一致性原则)
- 搜索条件固定为数据库可索引字段(`name` 列)
- 列表展示为三行式层级:`大字简名` → `(完整场馆名称)``📍 地址`
- JSON 内字段只做展示,不做搜索条件
```sql
-- ✅ 好:直接索引字段检索
WHERE name LIKE '%keyword%'
-- ❌ 坏JSON 字段模糊查询,效率低
WHERE JSON_EXTRACT(venue_data, '$.full_name') LIKE '%keyword%'
```
---
### 16. char 匹配需 `toUpperCase()` 归一化
座位标识字符比较时,务必归一化大小写:
```javascript
// ✅ 正确
const char = seatChar.toUpperCase();
const zone = zones.find(z => z.char.toUpperCase() === char);
// ❌ 错误:大小写不一致导致匹配失败
const zone = zones.find(z => z.char === seatChar);
```
---
## 📌 开发前检查清单
接手本插件时,逐项确认以下内容:
- [ ] `save.html` 有完整的 `{{:ModuleInclude('public/header')}}``{{:ModuleInclude('public/footer')}}`
- [ ] Vue 3 的 `<textarea>` 没有使用 `[[ ]]` 插值绑定 value
- [ ] Vue CDN 使用 `cdn.staticfile.net` 而非 `unpkg.com`
- [ ] Hook.php 返回数组包含 `id`、`url`、`name`、`is_show`
- [ ] Admin.php 有缓存锁机制保护 `initialize()` 免于每次请求检查表
- [ ] 改字段名之前查过 Service 层源码或实际表结构
- [ ] 插件视图路径使用 `../../../plugins/vr_ticket/view/...` 前缀
- [ ] `php -l` 语法检查通过后再提交