20 KiB
20 KiB
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)
- 删除 vr_events 表:活动信息直接用 ShopXO 商品替代(商品名称 = 活动名称)
- 删除 vr_sessions 表:场次 = ShopXO spec_value(每个规格值 = 一个演出时间)
- 删除 vr_venues 表:场馆/座位配置合并到
vr_seat_templates.venue_dataJSON - Phase 2 简化:不需要独立的 Event/Session CRUD,商家直接用 ShopXO 商品管理
- 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 | ✅ | 人工验收 |
验收标准
- 后台左侧菜单出现「VR票务」→「座位模板/电子票/核销记录」
- 点击「座位模板」不报错(可为空列表)
- 访问
/plugins/vr_ticket/admin/seat_template/list返回有效 JSON
五、Phase 1 — 数据库迁移文件
database/migrations/001_vr_seat_templates.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
-- =====================================================
-- 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
-- =====================================================
-- 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
-- =====================================================
-- 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)
{
"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
/**
* 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
/**
* 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'];
}
}
七、立即可执行的下一步
- 大头手动:安装 shopxo-uniapp(HBuilderX → Git 克隆 or 下载)
- 西莉娅生成:Phase 0 插件骨架 → 上传到 ShopXO 插件目录
- 大头验收:后台安装插件 → 验证菜单出现
- 妮可生成:Phase 1 数据库迁移 SQL(001-004)
- 李狗蛋(待大头搭好环境后):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 完成后 |