# ShopXO 插件静态文件同步陷阱 > **2026-04-24 票夹 API 请求 404 根因分析** > > 关键词:ShopXO、插件静态文件、public/ vs app/、Nginx root、docker cp、双目录陷阱 --- ## 现象 票夹页面加载正常,但所有 API 请求返回 404: ``` curl http://localhost:10000/plugins/vr_ticket//api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=list → 404 ``` 浏览器 Network 面板显示 JS 正在请求 `/plugins/vr_ticket//api.php`(双斜杠),但该路径不存在。 --- ## 根因:ShopXO 插件静态文件有**两套副本** ShopXO 插件文件在容器内存在于两个位置: | 路径 | 用途 | Nginx 是否 serve | |------|------|-----------------| | `/var/www/html/app/plugins/vr_ticket/` | PHP 运行时源码 | ❌ PHP 代码目录 | | `/var/www/html/public/plugins/vr_ticket/` | Nginx webroot 静态副本 | ✅ **Nginx 直接 serve** | **Nginx 的 `root` 指令是 `/var/www/html/public`**,因此所有浏览器请求的 URL 路径 `/plugins/vr_ticket/static/js/xxx.js` 都会被 Nginx 从 `public/plugins/` 目录读取。 PHP(ThinkPHP)运行时加载插件类文件时,使用 `app/plugins/` 目录。 ### 为什么会产生两份不同的文件? **正常情况(bind mount)**:如果 `app/` 和 `public/` 都是同一个 bind mount 的子目录(`/var/www/html` → host 的 shopxo 目录),它们应该是同一个源。 **异常情况**:当手动在容器内修改文件、Docker cp 上传文件、或者插件重装时,这两个目录可能产生分歧: 1. `docker cp` 直接写入容器路径 → 如果写入 `app/` 但 `public/` 是镜像层预置,则不同步 2. 插件重装时 ShopXO 同步静态文件到 `public/` 但没有同步 `app/` 3. 容器内手动编辑 `public/` 后没有更新 `app/` ### 验证方法 ```bash # 对比两边的 MD5(不一致 = 有问题) docker exec shopxo-php md5sum \ /var/www/html/app/plugins/vr_ticket/static/js/ticket_card.js \ /var/www/html/public/plugins/vr_ticket/static/js/ticket_card.js # 如果 MD5 不同,需要手动同步 docker cp /path/to/local/file.js \ shopxo-php:/var/www/html/public/plugins/vr_ticket/static/js/file.js # 同时也要更新 app/(运行时需要) docker cp /path/to/local/file.js \ shopxo-php:/var/www/html/app/plugins/vr_ticket/static/js/file.js ``` --- ## 本次 Case:ticket_card.js 的 apiBase 构造错误 ### 问题代码(ticket_card.js) ```javascript var apiBase = document.currentScript ? document.currentScript.src.replace(/static\/js\/[^/]+$/, '') + '/api.php?...': '/api.php?...'; ``` 当脚本被加载为 `/plugins/vr_ticket/static/js/ticket_card.js` 时: - `document.currentScript.src` = `http://localhost:10000/plugins/vr_ticket/static/js/ticket_card.js` - `replace(/static\/js\/[^/]+$/, '')` = `http://localhost:10000/plugins/vr_ticket/` - 拼接 `/api.php?...` → **`http://localhost:10000/plugins/vr_ticket/api.php?...`**(双斜杠,404) ### 修复方案 硬编码 `apiBase` 为绝对路径,绕过动态检测: ```javascript var apiBase = '/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction='; ``` ### 为什么之前修错了位置? 用 `docker cp` 更新了 `app/plugins/vr_ticket/static/js/ticket_card.js`,但浏览器被 Nginx serve 的是 `public/plugins/vr_ticket/static/js/ticket_card.js`(旧版)。直到把修复也同步到 `public/` 后才生效。 --- ## 经验总结 ### 修改插件静态文件的标准流程 1. **同时修改 `app/` 和 `public/` 两个副本**: ```bash # 方式A:先改源码,再同步副本 vim app/plugins/vr_ticket/static/js/xxx.js cp app/plugins/vr_ticket/static/js/xxx.js public/plugins/vr_ticket/static/js/xxx.js # 方式B:直接用 docker cp 同步到两边 docker cp local.js shopxo-php:/var/www/html/app/plugins/vr_ticket/static/js/xxx.js docker cp local.js shopxo-php:/var/www/html/public/plugins/vr_ticket/static/js/xxx.js ``` 2. **修改后立即验证 MD5**: ```bash docker exec shopxo-php md5sum \ /var/www/html/app/plugins/vr_ticket/static/js/xxx.js \ /var/www/html/public/plugins/vr_ticket/static/js/xxx.js # 两个 MD5 必须一致 ``` 3. **如果用了 ThinkPHP 模板缓存**,清理模板编译缓存: ```bash docker exec shopxo-php find /var/www/html/runtime/index/temp -name "*.php" -delete docker exec shopxo-php find /var/www/html/runtime/cache/shopxo -name "*.php" -delete ``` ### ShopXO 静态文件分布图 ``` 宿主机(bind mount 源) /Users/bigemon/WorkSpace/vr-shopxo-plugin/shopxo/ ├── app/plugins/vr_ticket/ ← PHP 运行时读取 │ └── static/js/ticket_card.js └── public/plugins/vr_ticket/ ← Nginx webroot(浏览器访问) └── static/js/ticket_card.js ↓ bind mount 容器 /var/www/html/ ├── app/plugins/vr_ticket/ ← PHP runtime └── public/plugins/vr_ticket/ ← Nginx root ``` ### 预防措施 - **不要在容器内直接修改文件**(用 `docker cp` 写 `app/` 后手动复制到 `public/`) - **插件重装后立即检查两边 MD5** - **如果 bind mount 正常,两边应该永远同步**;如果不同步,检查是否有额外的 Docker 镜像层覆盖了 `public/` --- ## 相关文档 - [EXPERIENCES.md](EXPERIENCES.md) — 踩坑经验汇总 - [DEPLOYMENT.md](DEPLOYMENT.md) — 部署文档 - [docs/09_SHOPXO_CACHE_HANDBOOK.md](09_SHOPXO_CACHE_HANDBOOK.md) — 缓存机制