vr-shopxo-plugin/docs/EXPERIENCES.md

9.1 KiB
Raw Blame History

VR票务插件 — 踩坑经验文档

本文档源自 2026-04-16 一整夜的重构调试,汇集了所有关键教训。 任何接手本项目的 agent请先阅读本文档。

原始日志:refactoring_log_vrticket_2026.md4644行 提炼源:refactoring_learnings.md


🔴 P0 — 致命陷阱(必读)

现象:列表页秒开,点击"新建/编辑"后页面永远转圈。 假线索后端死循环、数据库慢查询、unpkg.com CDN 阻断。 真实根因save.html 漏掉了 {{:ModuleInclude('public/footer')}}。AmazeUI 后台在页面跳转时显示全屏 Loading Spinner关闭动画的 JS 逻辑在 footer 的库文件里。没有 footer → Spinner 永不消失。

<!-- ❌ 错误:漏 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 补丁机制无限死循环。

<!-- ❌ 错误 -->
<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)。

// ❌ 错误:凭经验猜字段名
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/ 而非插件目录。

// ❌ 错误:引擎截断路径
return MyView('venue/list');

// ✅ 正确:跨模块绝对路径
return MyView('../../../plugins/vr_ticket/view/venue/list');

教训:插件视图必须放在插件根目录的 view/ 下(不是 admin/view/),且调用时加 ../../../plugins/插件名/view/... 前缀。


5. Hook.php 返回值必须完整

ShopXO 的菜单渲染引擎要求 Hook 返回数组包含 idurlnameis_show 完整字段。缺失任何一项都会导致侧边栏报错(Undefined array key url 等)。

// ❌ 错误:缺少字段
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 在中国大陆常发生静默阻断挂起。

<!-- ❌ 错误:国内阻断 -->
<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.netcdn.bootcdn.net,禁止 unpkg.com/cdnjs.cloudflare.com


7. PHP 注释块污染:未闭合 /* 导致语法错误

现象:整个 Admin 控制器报 syntax error, unexpected token "public"根因:调试期间遗留的未闭合 /* 注释块吞噬了后面的方法定义。

/* 这是调试代码...
// 忘记闭合public 关键字被吞掉
public function index() { ... }

教训:隔离测试时清理调试代码;用 php -l 做语法检查。


8. ShopXO 路由 → PluginsAdminUrl() 而非硬编码

// ❌ 错误:硬编码 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 次。

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

// ❌ 错误
$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

<!-- ❌ 错误:模板插值,含特殊字符时 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 会被截断。

// 前端:提交前 Base64 编码
formData.seat_map = btoa(unescape(encodeURIComponent(jsonString)));

// 后端:解码还原
$seatMap = json_decode(base64_decode($_POST['seat_map']), true);

15. 搜索字段 = 列表主标题(字段一致性原则)

  • 搜索条件固定为数据库可索引字段(name 列)
  • 列表展示为三行式层级:大字简名(完整场馆名称)📍 地址
  • JSON 内字段只做展示,不做搜索条件
-- ✅ 好:直接索引字段检索
WHERE name LIKE '%keyword%'

-- ❌ 坏JSON 字段模糊查询,效率低
WHERE JSON_EXTRACT(venue_data, '$.full_name') LIKE '%keyword%'

16. char 匹配需 toUpperCase() 归一化

座位标识字符比较时,务必归一化大小写:

// ✅ 正确
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 返回数组包含 idurlnameis_show
  • Admin.php 有缓存锁机制保护 initialize() 免于每次请求检查表
  • 改字段名之前查过 Service 层源码或实际表结构
  • 插件视图路径使用 ../../../plugins/vr_ticket/view/... 前缀
  • php -l 语法检查通过后再提交