# vr-shopxo-plugin 项目启动报告

> 生成时间：2026-04-15 00:16 CST  
> 生成方式：西莉娅 Council 协助（模型路由：opus，MiniMax 路由）  
> 关联 Issue：#TBD（创建后填入）

---

## 一、当前状态确认（Status Report）

### 1.1 代码 vs 文档差异

| 项目 | 文档状态（ARCHITECTURE.md v2.2） | 实际代码 |
|------|---|---|
| 核心架构 | spec = 场次，venue_data 精简 | ✅ 一致 |
| 插件表 | vr_seat_templates / vr_tickets / vr_verifiers / vr_verifications | ✅ 一致 |
| plugin.json | 应删除 event/session 管理菜单 | ❌ 仍为旧版菜单（vr_events/vr_sessions） |
| plan.md | 应删除 vr_events/vr_sessions 表 | ❌ 仍为旧 phase 结构 |
| PHP 代码 | 应有完整骨架（service/EventListener.php） | ❌ 零 PHP 代码 |
| 前端代码 | shopxo-uniapp 定制页面 | ❌ 零 Vue 代码 |
| 数据库迁移 | 应有 001-004 迁移文件 | ❌ 无 SQL 文件 |

### 1.2 待清理的旧内容

- [ ] `plan.md` 中的 Phase 1（旧 vr_events/vr_sessions 表结构）
- [ ] `docs/04_IMPLEMENTATION_ROADMAP.md` 中的旧 phase（旧表名）
- [ ] `plugin.json` 中的 `活动管理/场次管理` 菜单 → 改为 `座位模板/电子票/核销记录`

### 1.3 环境现状

| 组件 | 状态 | 地址 |
|------|------|------|
| ShopXO PHP 后端 | ✅ 运行中 | http://localhost:10000/ |
| ShopXO MySQL | ✅ 运行中 | localhost:10001 |
| shopxo-uniapp | ❌ 未安装 | — |
| 插件目录 | ✅ 存在 | `{SHOPXO_SRC}/app/plugins/` |
| vr_ticket 插件目录 | ❌ 为空 | — |

> `SHOPXO_SRC` = `/Users/bigemon/.openclaw/workspace/council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src`

---

## 二、实施路线图（Revised v2.2）

> 基于 ARCHITECTURE.md v2.2，删除 vr_events/vr_sessions/vr_venues 全部内容

### 精简后的 Phase 列表

| Phase | 内容 | 交付物 | 预估 | 可并行 |
|-------|------|--------|------|--------|
| **Phase 0** | 环境搭建 + 插件骨架 | 可安装的插件目录结构 | 1天 | — |
| **Phase 1** | 数据库迁移 | 4个 SQL 迁移文件 + 执行 | 0.5天 | ✅ Phase 0 |
| **Phase 2** | 后台 CRUD + API | SeatTemplate + Ticket 后台 + API | 3天 | ✅ Phase 1 |
| **Phase 3** | 前端选座 UI | 座位地图组件 + 选座页 | 3天 | ✅ Phase 2 |
| **Phase 4** | 订单钩子 + 观演人 | `plugins_service_buy_order_*` 钩子 | 2天 | ✅ Phase 3 |
| **Phase 5** | 支付回调 + QR 票 | `order.paid` 监听 → QR 生成 | 1天 | ✅ Phase 4 |
| **Phase 6** | B 端核销页 | 扫码核销页面 + API | 2天 | ✅ Phase 5 |
| **Phase 7** | 票夹 + C 端 | 用户票夹页 + 订单列表 | 2天 | ✅ Phase 6 |
| **Phase 8** | 联调 + 测试 + 部署 | 微信小程序审核上线 | 2天 | 串行 |

**预估**：Agent 集群并行 → **Phase 0-7 共约 7 天**，Phase 8 收尾共 9 天

### 核心变更说明（vs 旧版 roadmap）

1. **删除 vr_events 表**：活动信息直接用 ShopXO 商品替代（商品名称 = 活动名称）
2. **删除 vr_sessions 表**：场次 = ShopXO spec_value（每个规格值 = 一个演出时间）
3. **删除 vr_venues 表**：场馆/座位配置合并到 `vr_seat_templates.venue_data` JSON
4. **Phase 2 简化**：不需要独立的 Event/Session CRUD，商家直接用 ShopXO 商品管理
5. **Phase 5 新增**：QR 票生成 + 支付回调分离（Phase 4 只处理下单钩子，Phase 5 处理支付成功）

---

## 三、任务分配方案（Task Allocation）

### Agent 分工建议

| Phase | 主要负责 | 辅助 | 说明 |
|-------|---------|------|------|
| Phase 0 | **大头（手动）** | AI 生成清单 | 环境需要本地操作，AI 生成步骤清单 |
| Phase 1 | 妮可 | — | 数据库迁移脚本，CRUD 模式 |
| Phase 2 | 李狗蛋 | 妮可 | 后台 CRUD + API，可并行 |
| Phase 3 | 李狗蛋 | 大头（验收） | 前端选座组件，shopxo-uniapp |
| Phase 4 | 李狗蛋 | — | 订单钩子 + 观演人表单 |
| Phase 5 | 西莉娅 | — | 支付回调 + QR 生成（熟悉 ShopXO Hook） |
| Phase 6 | 西莉娅 | — | B 端核销页（参考 realstore/check.vue） |
| Phase 7 | 西莉娅 | 大头（验收） | 票夹 + C 端 |
| Phase 8 | 大头 | 全员 | 联调 + 微信审核上线 |

### 大头（Bigemon）职责

- **Phase 0**：本地操作（安装 shopxo-uniapp、配置 HBuilderX、安装插件到 ShopXO）
- **Phase 3/7 验收**：前端 UI/UX 体验把关
- **Phase 8 主协调**：联调问题定位 + 微信审核沟通
- **全局**：架构决策拍板、技术债务审查

### 优先级排序（先做什么价值最大）

```
P0（立即可做）
└── Phase 0：环境 + 插件骨架 → 无此则后续无法运行

P1（骨架完成后立即启动）
├── Phase 1：数据库迁移 → 提供所有表的 SQL
└── Phase 2：后台 API → 其他 phase 依赖

P2（Phase 2 完成后并行）
├── Phase 3：前端选座 → 用户核心体验
├── Phase 4：订单钩子 → 核心购票流程
└── Phase 5：QR 票 → 购票交付物

P3（后半程）
├── Phase 6：B 端核销 → 商家核心功能
└── Phase 7：票夹 → 用户核心功能
```

---

## 四、Phase 0 详细实施计划

### 环境信息

```
ShopXO 后台：http://localhost:10000/admin
ShopXO 前台：http://localhost:10000/
MySQL：localhost:10001（root/ShopXO@2026）
SHOPXO_SRC：/Users/bigemon/.openclaw/workspace/council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src
插件目录：{SHOPXO_SRC}/app/plugins/vr_ticket/
shopxo-uniapp：需新建（目录 TBD，建议放在 shopxo-env/ 下）
```

### Step by Step

- [ ] **Step 0.1**：验证 ShopXO 后台可访问 → http://localhost:10000/admin
- [ ] **Step 0.2**：下载 shopxo-uniapp（HBuilderX → Git 克隆 or 下载 zip）
- [ ] **Step 0.3**：配置 `shopxo-uniapp/common/config.js` 的 `request_url` = `http://localhost:10000/`
- [ ] **Step 0.4**：HBuilderX 导入 shopxo-uniapp → 本地 H5 预览验证
- [ ] **Step 0.5**：创建插件目录结构（见下方目录树）
- [ ] **Step 0.6**：写入 `plugin.json`（更新版，对齐 v2.2）
- [ ] **Step 0.7**：写入 `EventListener.php` + `service/BaseService.php`（骨架）
- [ ] **Step 0.8**：后台 → 插件管理 → 找到 vr_ticket → 点击安装
- [ ] **Step 0.9**：后台左侧菜单是否出现「VR票务」菜单

### AI 可参与度

| Step | AI 可做 | 需要大头手动 |
|------|---------|------------|
| 0.1 | ✅ | 验证 URL |
| 0.2 | ✅（生成 git clone 命令） | HBuilderX 操作 |
| 0.3 | ✅（生成配置代码） | 打开 HBuilderX |
| 0.4 | ❌ | HBuilderX 预览 |
| 0.5 | ✅（生成 mkdir 命令） | 执行命令 |
| 0.6 | ✅（生成完整 JSON） | 上传到服务器 |
| 0.7 | ✅（生成完整 PHP） | 上传到服务器 |
| 0.8 | ❌ | 浏览器操作 |
| 0.9 | ✅ | 人工验收 |

### 验收标准

1. 后台左侧菜单出现「VR票务」→「座位模板/电子票/核销记录」
2. 点击「座位模板」不报错（可为空列表）
3. 访问 `/plugins/vr_ticket/admin/seat_template/list` 返回有效 JSON

---

## 五、Phase 1 — 数据库迁移文件

### `database/migrations/001_vr_seat_templates.sql`

```sql
-- =====================================================
-- VR票务插件 - 座位模板表
-- 座位模板通过分类ID绑定到 ShopXO 商品
-- AI 参与度：100%（标准建表语句）
-- =====================================================

CREATE TABLE IF NOT EXISTS `vr_seat_templates` (
    `id`           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '模板ID',
    `name`         VARCHAR(180)    NOT NULL            COMMENT '模板名称（如：鸟巢-A区）',
    `category_id`  BIGINT UNSIGNED NOT NULL            COMMENT '绑定的 ShopXO 分类ID',
    `seat_map`     LONGTEXT       NOT NULL            COMMENT '座位地图JSON（见 venue_data 结构）',
    `spec_base_id_map` LONGTEXT  DEFAULT NULL         COMMENT '座位ID→spec_base_id 映射JSON',
    `status`       TINYINT UNSIGNED DEFAULT 1          COMMENT '状态：0禁用 1启用',
    `add_time`     INT UNSIGNED    DEFAULT 0           COMMENT '创建时间戳',
    `upd_time`     INT UNSIGNED    DEFAULT 0           COMMENT '更新时间戳',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  COMMENT='VR演唱会座位模板';

-- seat_map JSON 结构示例：
-- {
--   "map": ["aaaaaaaaaaaa", "aaaaaaaaaaaa", "bbbbbb__bb"],
--   "row_labels": ["A", "B", "C"],
--   "seats": {
--     "a": { "price": 599, "label": "VIP区", "classes": "seat-vip" },
--     "b": { "price": 399, "label": "普通区", "classes": "seat-normal" },
--     "_": null
--   },
--   "sections": [
--     { "name": "VIP区", "color": "#FF6B6B", "rows": [0, 1] },
--     { "name": "普通区", "color": "#4ECDC4", "rows": [2, 3] }
--   ]
-- }
```

### `database/migrations/002_vr_tickets.sql`

```sql
-- =====================================================
-- VR票务插件 - 电子票表
-- 支付成功后由 TicketService 写入
-- AI 参与度：100%（标准建表语句）
-- =====================================================

CREATE TABLE IF NOT EXISTS `vr_tickets` (
    `id`             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '票ID',
    `order_id`       BIGINT UNSIGNED NOT NULL               COMMENT 'ShopXO 订单ID',
    `order_no`       CHAR(60)       NOT NULL                COMMENT '订单号',
    `goods_id`       BIGINT UNSIGNED NOT NULL               COMMENT '商品ID',
    `goods_snapshot` TEXT           DEFAULT NULL           COMMENT '商品快照JSON（规格名/场次名）',
    `user_id`        BIGINT UNSIGNED NOT NULL               COMMENT '用户ID',
    `ticket_code`    CHAR(36)      NOT NULL                COMMENT 'UUID 票码',
    `qr_data`        TEXT           DEFAULT NULL            COMMENT '加密QR内容',
    `seat_info`      VARCHAR(255)   DEFAULT NULL            COMMENT '座位信息（如 A区-3排-5座）',
    `spec_base_id`   BIGINT UNSIGNED DEFAULT 0              COMMENT 'spec_base_id（关联ShopXO规格）',
    `real_name`      VARCHAR(60)    DEFAULT NULL            COMMENT '观演人姓名',
    `phone`          CHAR(15)       DEFAULT NULL            COMMENT '观演人手机',
    `id_card`        CHAR(18)       DEFAULT NULL            COMMENT '观演人身份证（选填）',
    `verify_status`  TINYINT UNSIGNED DEFAULT 0             COMMENT '核销状态：0未核销 1已核销 2已退款',
    `verify_time`    INT UNSIGNED   DEFAULT 0              COMMENT '核销时间戳',
    `verifier_id`    BIGINT UNSIGNED DEFAULT 0              COMMENT '核销员ID（vr_verifiers.id）',
    `issued_at`      INT UNSIGNED   DEFAULT 0              COMMENT '票发放时间戳（支付成功后）',
    `created_at`     INT UNSIGNED   DEFAULT 0              COMMENT '记录创建时间',
    `updated_at`     INT UNSIGNED   DEFAULT 0              COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_ticket_code` (`ticket_code`),
    KEY `idx_order_id` (`order_id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_goods_id` (`goods_id`),
    KEY `idx_verify_status` (`verify_status`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  COMMENT='VR演唱会电子票';
```

### `database/migrations/003_vr_verifiers.sql`

```sql
-- =====================================================
-- VR票务插件 - 核销员表
-- AI 参与度：100%（标准建表语句）
-- =====================================================

CREATE TABLE IF NOT EXISTS `vr_verifiers` (
    `id`         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '核销员ID',
    `user_id`    BIGINT UNSIGNED NOT NULL                COMMENT '对应的 ShopXO 用户ID',
    `name`       VARCHAR(60)    NOT NULL                  COMMENT '核销员名称',
    `status`     TINYINT UNSIGNED DEFAULT 1               COMMENT '状态：0禁用 1启用',
    `created_at` INT UNSIGNED   DEFAULT 0                 COMMENT '创建时间',
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  COMMENT='VR票务核销员';
```

### `database/migrations/004_vr_verifications.sql`

```sql
-- =====================================================
-- VR票务插件 - 核销记录表
-- AI 参与度：100%（标准建表语句）
-- =====================================================

CREATE TABLE IF NOT EXISTS `vr_verifications` (
    `id`             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '核销记录ID',
    `ticket_id`      BIGINT UNSIGNED NOT NULL               COMMENT '票ID（vr_tickets.id）',
    `ticket_code`    CHAR(36)      NOT NULL                 COMMENT '票码（冗余存储）',
    `verifier_id`    BIGINT UNSIGNED NOT NULL               COMMENT '核销员ID',
    `verifier_name`  VARCHAR(60)   DEFAULT NULL             COMMENT '核销员名称（冗余）',
    `goods_id`       BIGINT UNSIGNED DEFAULT 0              COMMENT '商品ID',
    `created_at`     INT UNSIGNED   DEFAULT 0               COMMENT '核销时间',
    PRIMARY KEY (`id`),
    KEY `idx_ticket_id` (`ticket_id`),
    KEY `idx_verifier_id` (`verifier_id`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  COMMENT='VR票务核销记录';
```

---

## 六、插件骨架代码

### `plugin.json`（更新版，对齐 v2.2）

```json
{
    "name": "vr-ticket",
    "title": "VR票务",
    "description": "为ShopXO添加VR演唱会票务功能：座位模板、观演人收集、QR电子票、扫码核销",
    "version": "1.0.0",
    "author": "sileya-ai",
    "author_url": "http://xmhome.ow-my.com:3000/sileya-ai/vr-shopxo-plugin",
    "shopxo_version": ">=1.0.0",
    "dependencies": [],
    "menus": [
        {
            "title": "VR票务",
            "icon": "icon icon-ticket",
            "submenus": [
                { "title": "座位模板", "url": "/plugins/vr-ticket/admin/seat_template/list" },
                { "title": "电子票管理", "url": "/plugins/vr-ticket/admin/ticket/list" },
                { "title": "核销记录", "url": "/plugins/vr-ticket/admin/verification/list" }
            ]
        }
    ],
    "listen_events": [
        "order.paid"
    ],
    "hooks": [
        "onOrderPaid"
    ]
}
```

### `EventListener.php`

```php
<?php
/**
 * VR票务插件 - 事件监听器
 *
 * ShopXO 事件监听入口
 * 监听 order.paid 事件 → 触发 QR 票生成
 *
 * @package vr-ticket
 */

/**
 * 订单支付成功事件处理
 *
 * @param array $params 事件参数，含 order_id / order_no / user_id
 * @return bool
 */
function vr_ticket_on_order_paid($params = [])
{
    // 引入 TicketService
    use_app_service('vr_ticket/TicketService');

    try {
        $order_id = $params['order_id'] ?? 0;
        if (empty($order_id)) {
            return false;
        }

        // 调用票服务：生成并发放 QR 票
        $result = \app\plugins\vr_ticket\service\TicketService::OnOrderPaid($order_id, $params);

        return $result !== false;
    } catch (\Exception $e) {
        // 记录错误，不阻塞订单流程
        log_info('[vr-ticket] OrderPaid Error: ' . $e->getMessage(), $params);
        return false;
    }
}
```

### `service/BaseService.php`

```php
<?php
/**
 * VR票务插件 - 基础服务
 *
 * 提供通用工具方法：加密/解密、日志、时间戳等
 *
 * @package vr-ticket/service
 */

namespace app\plugins\vr_ticket\service;

class BaseService
{
    /**
     * 获取插件根目录
     * @return string
     */
    public static function getPluginPath()
    {
        return ROOT_PATH . 'app' . DS . 'plugins' . DS . 'vr_ticket' . DS;
    }

    /**
     * 获取插件静态资源URL
     * @param string $path 相对于 static/ 目录的路径
     * @return string
     */
    public static function getStaticUrl($path = '')
    {
        return ROOT_URL . 'app/plugins/vr_ticket/static/' . ltrim($path, '/');
    }

    /**
     * 当前时间戳
     * @return int
     */
    public static function now()
    {
        return time();
    }

    /**
     * 生成 UUID v4 票码
     * @return string
     */
    public static function generateUuid()
    {
        $data = random_bytes(16);
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }

    /**
     * AES-256-CBC 加密票务数据
     *
     * @param array $data 待加密数据
     * @param int $expire 过期时间戳（默认30天）
     * @return string base64 编码的密文
     */
    public static function encryptTicketData($data, $expire = null)
    {
        $secret = self::getTicketSecret();
        $expire = $expire ?? (time() + 86400 * 30);

        $payload = json_encode(array_merge($data, [
            'exp' => $expire,
            'iat' => time(),
        ]), JSON_UNESCAPED_UNICODE);

        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);

        $combined = $iv . $encrypted;
        return base64_encode($combined);
    }

    /**
     * 解密票务数据
     *
     * @param string $encoded base64 编码的密文
     * @return array|null 解密失败返回 null
     */
    public static function decryptTicketData($encoded)
    {
        $secret = self::getTicketSecret();
        $combined = base64_decode($encoded);

        if (strlen($combined) < 16) {
            return null;
        }

        $iv = substr($combined, 0, 16);
        $encrypted = substr($combined, 16);

        $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);

        if ($decrypted === false) {
            return null;
        }

        $data = json_decode($decrypted, true);

        // 检查过期
        if (isset($data['exp']) && $data['exp'] < time()) {
            return null;
        }

        return $data;
    }

    /**
     * 获取票务加密密钥
     * 优先从环境变量读取，否则用 ShopXO app_secret
     * @return string
     */
    private static function getTicketSecret()
    {
        $secret = env('VR_TICKET_SECRET', '');
        if (!empty($secret)) {
            return $secret;
        }
        // 回退：使用 ShopXO 应用密钥
        return config('shopxo.app_key', 'shopxo_default_secret_change_me');
    }

    /**
     * 安全的日志写入
     *
     * @param string $message 日志信息
     * @param array $context 上下文数据
     * @param string $level info|error|warning
     */
    public static function log($message, $context = [], $level = 'info')
    {
        $tag = '[vr-ticket]';
        $ctx = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
        log_{$level}($tag . $message . $ctx);
    }

    /**
     * 判断商品是否为票务商品
     *
     * @param int $goods_id 商品ID
     * @return bool
     */
    public static function isTicketGoods($goods_id)
    {
        $goods = \app\model\Goods::find($goods_id);
        if (empty($goods)) {
            return false;
        }
        return !empty($goods['venue_data']);
    }

    /**
     * 获取商品 venue_data
     *
     * @param int $goods_id
     * @return array
     */
    public static function getGoodsVenueData($goods_id)
    {
        $goods = \app\model\Goods::find($goods_id);
        if (empty($goods) || empty($goods['venue_data'])) {
            return [];
        }
        return is_string($goods['venue_data']) ? json_decode($goods['venue_data'], true) : $goods['venue_data'];
    }
}
```

---

## 七、立即可执行的下一步

1. **大头手动**：安装 shopxo-uniapp（HBuilderX → Git 克隆 or 下载）
2. **西莉娅生成**：Phase 0 插件骨架 → 上传到 ShopXO 插件目录
3. **大头验收**：后台安装插件 → 验证菜单出现
4. **妮可生成**：Phase 1 数据库迁移 SQL（001-004）
5. **李狗蛋**（待大头搭好环境后）：Phase 2 后台 CRUD

---

## 八、分工确认（待大头回复）

请确认以下分工是否符合预期：

| Phase | 主要负责人 | 预计开始 |
|-------|----------|---------|
| Phase 0 | 大头（手动操作） | **立即** |
| Phase 1 | 妮可（SQL 迁移） | Phase 0 完成后 |
| Phase 2 | 李狗蛋（后台 CRUD） | Phase 1 完成后 |
| Phase 3 | 李狗蛋 + 大头（验收） | Phase 2 完成后 |
| Phase 4 | 李狗蛋（订单钩子） | Phase 3 中 |
| Phase 5 | 西莉娅（QR 生成） | Phase 4 中 |
| Phase 6 | 西莉娅（B端核销） | Phase 5 中 |
| Phase 7 | 西莉娅 + 大头（票夹） | Phase 6 中 |
| Phase 8 | 大头（联调上线） | Phase 7 完成后 |
