# 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) — 缓存机制
