570 lines
20 KiB
Markdown
570 lines
20 KiB
Markdown
|
|
# 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 完成后 |
|