fix(goods-vr-ticket-order): 修复 H5 刷新页面导致参数丢失的问题
问题原因: H5 SPA 模式下,带有复杂 JSON 数据的 URL 参数(seats, selection, buyNotices 等) 在刷新时会被浏览器/路由框架错误解析,导致参数截断丢失。 解决方案: - 参考 buy.vue 的做法,将跳转参数通过 base64 编码打包为单一 data 参数传递 - 在订单确认页 onLoad 中解包 data 参数,并兼容处理单重/双重编码场景 - 新增 safeParseJson 和 safeDecodeURIComponent 辅助方法提高健壮性 修改文件: - pages/goods-vr-ticket/goods-vr-ticket.vue (goToOrder 跳转逻辑) - pages/goods-vr-ticket-order/goods-vr-ticket-order.vue (onLoad 数据解析)master
parent
29e6f0a6d0
commit
4c2c9a3ed6
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,732 @@
|
|||
# 观影人信息管理功能实现方案(修订版)
|
||||
|
||||
## 注意事项
|
||||
以下内容里的“vrt_”为数据表prefix,会自动处理,在插件使用中,一般建议不要写死而是靠shopxo自动处理。
|
||||
|
||||
## 目标分解
|
||||
|
||||
根据业务优先级,分为两个独立阶段:
|
||||
|
||||
### 🔥 阶段 1:订单确认页观影人信息采集(高优先级)
|
||||
|
||||
**核心需求**:
|
||||
- 在订单确认页强制填写观影人手机号(如果商品配置要求)
|
||||
- 直接将观影人信息写入票据表 `vrt_vr_tickets`
|
||||
- 无需独立的观影人管理功能
|
||||
- **后端改动极小,前端快速实现**
|
||||
|
||||
### 📋 阶段 2:用户观影人通讯录管理(低优先级,TODO)
|
||||
|
||||
**核心需求**:
|
||||
- 用户可以保存常用观影人信息
|
||||
- 在订单确认页快速选择历史观影人
|
||||
- 提供独立的观影人管理页面(增删改查)
|
||||
- 需要前后端较大改动
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1 实现方案(本次实施)
|
||||
|
||||
### 业务流程
|
||||
|
||||
```
|
||||
用户选择商品 → 进入订单确认页
|
||||
→ 检查商品是否需要观影人信息
|
||||
→ [是] 前端展示观影人表单(姓名 + 手机号必填)
|
||||
→ 用户填写 → 提交订单
|
||||
→ 后端接收 viewer_data 参数
|
||||
→ 出票时写入 vrt_vr_tickets 表
|
||||
```
|
||||
|
||||
### 用户审查要点
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **阶段 1 核心设计**
|
||||
>
|
||||
> **灵活的字段控制**:
|
||||
> - 商品编辑页新增票务选项,包含 5 个独立开关:
|
||||
> 1. `require_viewer` - 是否需要填写观影人信息(总开关)
|
||||
> 2. `require_viewer_per_seat` - 是否每个座位独立填写观影人(阶段1默认关闭)
|
||||
> 3. `require_viewer_mobile` - 是否必填手机号
|
||||
> 4. `require_viewer_name` - 是否必填姓名
|
||||
> 5. `require_viewer_idcard` - 是否必填身份证号(预留)
|
||||
>
|
||||
> **校验逻辑**:
|
||||
> - 当 `require_viewer=1` 时,至少需要 `require_viewer_mobile=1`(手机号是核心字段)
|
||||
> - 其他字段(姓名、身份证)根据商家配置独立控制
|
||||
> - 当 `require_viewer_per_seat=1` 时,观影人数量必须等于出票数量
|
||||
>
|
||||
> **业务优势**:
|
||||
> - 不同场景灵活配置(音乐会可能只要手机号,戏剧演出可能需要实名)
|
||||
> - 手机号用于快速订单筛选和票务状态筛选
|
||||
> - 降低用户填写负担,提升转化率
|
||||
> - 数据结构支持未来扩展到多人多票场景
|
||||
|
||||
> [!NOTE]
|
||||
> **数据结构设计考虑**
|
||||
>
|
||||
> **阶段 1 实现**:
|
||||
> - `require_viewer_per_seat=0`(默认):前端提交单个观影人,后端复制到所有票据
|
||||
> - 数据格式:使用数组结构,但阶段1只包含一个元素
|
||||
>
|
||||
> **未来扩展**(阶段 2):
|
||||
> - `require_viewer_per_seat=1`:前端提交观影人数组,每个座位对应一个观影人
|
||||
> - 数据格式:数组结构无需改动,直接支持多元素
|
||||
>
|
||||
> **扩展性保障**:采用数组结构,平滑过渡到多人场景
|
||||
|
||||
### 数据库设计
|
||||
|
||||
> [!NOTE]
|
||||
> **插件架构说明**
|
||||
>
|
||||
> 我们只修改 VR Ticket 插件代码,不动 ShopXO 核心。
|
||||
>
|
||||
> **现有字段**:
|
||||
> - `vrt_goods.vr_goods_config` LONGTEXT - 插件已有的票务配置字段(JSON 存储)
|
||||
> - `vrt_goods.item_type` - 商品类型标识(插件设置为 'ticket')
|
||||
>
|
||||
> **数据流向**:
|
||||
> 1. 后台商品编辑页 → Hook 注入 Vue 表单
|
||||
> 2. 表单提交 → `vr_goods_config_base64` 隐藏字段
|
||||
> 3. `AdminGoodsSaveHandle` Hook 解码写入 `vr_goods_config`
|
||||
|
||||
#### 1. 商品配置字段(复用现有)
|
||||
|
||||
**存储位置**:`vrt_goods.vr_goods_config`(JSON 格式)
|
||||
|
||||
**当前结构**(已有):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"template_id": 1,
|
||||
"selected_rooms": ["room_0"],
|
||||
"selected_sections": {"room_0": ["A", "B"]},
|
||||
"sessions": [
|
||||
{"start": "08:00", "end": "23:59"}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**新增观影人配置**(扩展):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"template_id": 1,
|
||||
"selected_rooms": ["room_0"],
|
||||
"selected_sections": {"room_0": ["A", "B"]},
|
||||
"sessions": [{"start": "08:00", "end": "23:59"}],
|
||||
|
||||
// ===== 新增观影人配置 =====
|
||||
"viewer_config": {
|
||||
"require_viewer": 1, // 是否需要观影人信息(总开关)
|
||||
"require_viewer_per_seat": 0, // 是否每个座位独立填写(阶段1=0)
|
||||
"require_viewer_mobile": 1, // 是否必填手机号(默认1)
|
||||
"require_viewer_name": 0, // 是否必填姓名(默认0)
|
||||
"require_viewer_idcard": 0 // 是否必填身份证号(默认0)
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 每个场馆配置(`template_id`)可以有独立的观影人要求
|
||||
- 如果某个场馆没有 `viewer_config`,则不需要观影人信息
|
||||
- 保持向后兼容:现有数据没有 `viewer_config` 字段不影响功能
|
||||
|
||||
#### 2. 票据表(复用现有)
|
||||
|
||||
**表名**:`vrt_vr_tickets`
|
||||
|
||||
无需新增表或字段,直接使用:
|
||||
- `real_name` VARCHAR(60) - 观影人姓名
|
||||
- `phone` CHAR(15) - 观影人手机号
|
||||
- `id_card` CHAR(18) - 观影人身份证号(阶段1预留)
|
||||
|
||||
### API 接口设计
|
||||
|
||||
#### 1. 商品详情接口增强
|
||||
|
||||
**接口**:`/api/goods/detail`(或现有的商品详情接口)
|
||||
|
||||
**返回数据新增字段**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"goods": {
|
||||
// ... 现有商品字段
|
||||
"vr_ticket_config": {
|
||||
"require_viewer": 1,
|
||||
"require_viewer_mobile": 1,
|
||||
"require_viewer_name": 0,
|
||||
"require_viewer_idcard": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:前端根据此配置决定是否展示观影人表单,以及哪些字段必填
|
||||
|
||||
#### 2. 订单提交接口修改
|
||||
|
||||
**新增请求参数**(数组结构,支持未来扩展):
|
||||
```json
|
||||
{
|
||||
"goods_data": "...", // 现有参数
|
||||
"payment_id": 1, // 现有参数
|
||||
"viewer_data": [ // 新增:观影人数据数组
|
||||
{
|
||||
"name": "张三", // 观影人姓名(根据商品配置决定是否必填)
|
||||
"mobile": "13800138000", // 观影人手机号(根据商品配置决定是否必填)
|
||||
"idcard": "" // 观影人身份证号(阶段1预留)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- **阶段 1**:`viewer_data` 数组只包含 1 个元素(所有座位共用)
|
||||
- **未来扩展**:当 `require_viewer_per_seat=1` 时,数组长度等于座位数量
|
||||
- 数据结构一致,无需修改接口定义
|
||||
|
||||
**校验逻辑**:
|
||||
1. 从商品配置读取 `require_viewer` 和 `require_viewer_per_seat` 标志
|
||||
2. 如果 `require_viewer=1`:
|
||||
- 校验 `viewer_data` 数组不为空
|
||||
- 如果 `require_viewer_per_seat=0`:校验数组长度为 1
|
||||
- 如果 `require_viewer_per_seat=1`:校验数组长度等于购买座位数量(阶段1暂不启用)
|
||||
- 遍历数组,根据配置校验每个观影人的字段
|
||||
3. 手机号格式校验:`/^1[3-9]\d{9}$/`
|
||||
4. 身份证号格式校验:`/^\d{17}[\dXx]$/`(阶段1预留)
|
||||
|
||||
### 代码实现计划
|
||||
|
||||
#### 步骤 0:后台商品配置界面(修改插件 Hook)
|
||||
|
||||
**文件**:[AdminGoodsSave.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/.worktrees/FrontendDeveloper/shopxo/app/plugins/vr_ticket/hook/AdminGoodsSave.php)
|
||||
|
||||
**改动位置**:在 Vue 表单的场馆配置区域内,新增观影人配置选项
|
||||
|
||||
**具体修改**:在第 96-152 行(场馆配置循环内),场次设置之后,新增观影人配置区块:
|
||||
|
||||
```html
|
||||
<!-- 在场次管理之后,演播厅选择之前添加 -->
|
||||
<div class="am-form-group" style="background: #f0f8ff; padding: 12px; border: 1px solid #b0d4f1; border-radius: 4px; margin-bottom:15px;">
|
||||
<label style="font-weight:bold;">
|
||||
观影人信息设置
|
||||
<span style="color:#999; font-size:12px; font-weight:normal; margin-left:8px;">(票务实名制选项)</span>
|
||||
</label>
|
||||
|
||||
<div style="margin-top:10px;">
|
||||
<label class="am-checkbox-inline">
|
||||
<input type="checkbox" v-model="config.viewer_config.require_viewer" />
|
||||
<span style="margin-left:5px;">需要填写观影人信息</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-show="config.viewer_config.require_viewer" style="margin-left:20px; margin-top:10px; padding-left:15px; border-left:3px solid #ddd;">
|
||||
<div style="margin-bottom:8px;">
|
||||
<label class="am-checkbox-inline">
|
||||
<input type="checkbox" v-model="config.viewer_config.require_viewer_mobile" disabled checked />
|
||||
<span style="margin-left:5px;">必填手机号</span>
|
||||
<small style="color:#999; margin-left:8px;">(核心字段,默认必填)</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:8px;">
|
||||
<label class="am-checkbox-inline">
|
||||
<input type="checkbox" v-model="config.viewer_config.require_viewer_name" />
|
||||
<span style="margin-left:5px;">必填姓名</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:8px;">
|
||||
<label class="am-checkbox-inline">
|
||||
<input type="checkbox" v-model="config.viewer_config.require_viewer_idcard" />
|
||||
<span style="margin-left:5px;">必填身份证号</span>
|
||||
<small style="color:#999; margin-left:8px;">(预留功能)</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:8px;">
|
||||
<label class="am-checkbox-inline">
|
||||
<input type="checkbox" v-model="config.viewer_config.require_viewer_per_seat" disabled />
|
||||
<span style="margin-left:5px;">每个座位独立填写观影人</span>
|
||||
<small style="color:#999; margin-left:8px;">(阶段2功能,暂不可用)</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Vue setup() 修改**:
|
||||
|
||||
1. 在 `onTemplateChange` 函数中,初始化 `viewer_config` 默认值:
|
||||
|
||||
```javascript
|
||||
const onTemplateChange = () => {
|
||||
const oldConfigs = [...configs.value];
|
||||
configs.value = selectedTemplateIds.value.map(tid => {
|
||||
const existing = oldConfigs.find(c => c.template_id === tid);
|
||||
if (existing) return existing;
|
||||
return {
|
||||
template_id: tid,
|
||||
selected_rooms: [],
|
||||
selected_sections: {},
|
||||
sessions: defaultSessions(),
|
||||
// 新增:观影人配置默认值
|
||||
viewer_config: {
|
||||
require_viewer: 0,
|
||||
require_viewer_per_seat: 0,
|
||||
require_viewer_mobile: 1, // 默认必填
|
||||
require_viewer_name: 0,
|
||||
require_viewer_idcard: 0
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
2. 在配置还原部分(第 209-242 行),确保 `viewer_config` 正确还原:
|
||||
|
||||
```javascript
|
||||
configs.value = AppData.vrGoodsConfig
|
||||
.filter(c => validTemplateIds.has(c.template_id))
|
||||
.map(c => {
|
||||
// ... 现有逻辑
|
||||
|
||||
// 新增:确保 viewer_config 结构完整
|
||||
if (!c.viewer_config) {
|
||||
c.viewer_config = {
|
||||
require_viewer: 0,
|
||||
require_viewer_per_seat: 0,
|
||||
require_viewer_mobile: 1,
|
||||
require_viewer_name: 0,
|
||||
require_viewer_idcard: 0
|
||||
};
|
||||
}
|
||||
|
||||
return c;
|
||||
});
|
||||
```
|
||||
|
||||
**无需修改**:`outputBase64` 计算属性会自动包含 `viewer_config` 字段
|
||||
|
||||
#### 步骤 1:修改订单提交接口
|
||||
|
||||
**文件**:[BuyService.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/.worktrees/FrontendDeveloper/shopxo/app/service/BuyService.php#L655-L877)
|
||||
|
||||
**方法**:`OrderInsert`
|
||||
|
||||
**改动**:
|
||||
1. 接收 `viewer_data` 数组参数
|
||||
2. 获取订单中商品的 VR 票务配置
|
||||
3. 根据配置进行动态校验(数组结构)
|
||||
|
||||
```php
|
||||
// 在 OrderInsert 方法中添加
|
||||
// 1. 获取商品的观影人配置
|
||||
$goods_list = $this->BuyTypeGoodsList($params);
|
||||
$viewer_config = null;
|
||||
foreach ($goods_list as $goods) {
|
||||
if (!empty($goods['extends']['vr_ticket'])) {
|
||||
$viewer_config = $goods['extends']['vr_ticket'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 观影人信息校验
|
||||
if (!empty($viewer_config['require_viewer']) && $viewer_config['require_viewer'] == 1) {
|
||||
// 检查 viewer_data 是否存在且为数组
|
||||
if (empty($params['viewer_data']) || !is_array($params['viewer_data'])) {
|
||||
return DataReturn('请填写观影人信息', -1);
|
||||
}
|
||||
|
||||
// 阶段1:只支持单个观影人(require_viewer_per_seat=0)
|
||||
$per_seat = !empty($viewer_config['require_viewer_per_seat']) ? $viewer_config['require_viewer_per_seat'] : 0;
|
||||
if ($per_seat == 0) {
|
||||
// 单个观影人模式:数组长度必须为1
|
||||
if (count($params['viewer_data']) != 1) {
|
||||
return DataReturn('当前商品只需填写一个观影人信息', -1);
|
||||
}
|
||||
} else {
|
||||
// 多个观影人模式:数组长度必须等于购买数量(阶段1暂不启用)
|
||||
// 未来实现:校验 count($params['viewer_data']) == 购买座位数量
|
||||
return DataReturn('暂不支持每个座位独立填写观影人', -1);
|
||||
}
|
||||
|
||||
// 3. 遍历校验每个观影人的字段
|
||||
foreach ($params['viewer_data'] as $index => $viewer) {
|
||||
// 手机号校验
|
||||
if (!empty($viewer_config['require_viewer_mobile']) && $viewer_config['require_viewer_mobile'] == 1) {
|
||||
if (empty($viewer['mobile'])) {
|
||||
return DataReturn('请填写观影人手机号', -1);
|
||||
}
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $viewer['mobile'])) {
|
||||
return DataReturn('手机号格式不正确', -1);
|
||||
}
|
||||
}
|
||||
|
||||
// 姓名校验
|
||||
if (!empty($viewer_config['require_viewer_name']) && $viewer_config['require_viewer_name'] == 1) {
|
||||
if (empty($viewer['name'])) {
|
||||
return DataReturn('请填写观影人姓名', -1);
|
||||
}
|
||||
}
|
||||
|
||||
// 身份证号校验(阶段1预留)
|
||||
if (!empty($viewer_config['require_viewer_idcard']) && $viewer_config['require_viewer_idcard'] == 1) {
|
||||
if (empty($viewer['idcard'])) {
|
||||
return DataReturn('请填写观影人身份证号', -1);
|
||||
}
|
||||
if (!preg_match('/^\d{17}[\dXx]$/', $viewer['idcard'])) {
|
||||
return DataReturn('身份证号格式不正确', -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 将观影人信息传递给订单数据(保持数组结构)
|
||||
$params['viewer_info'] = $params['viewer_data'];
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 2:修改票务出票流程
|
||||
|
||||
**文件**:[TicketService.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/.worktrees/FrontendDeveloper/shopxo/app/plugins/vr_ticket/service/TicketService.php#L146-L238)
|
||||
|
||||
**方法**:`issueTicket`
|
||||
|
||||
**改动**:
|
||||
1. 接收订单数据中的 `viewer_info` 数组参数
|
||||
2. 在生成票据时,将观影人信息分配到对应的票据
|
||||
|
||||
```php
|
||||
// 在 issueTicket 方法中
|
||||
// 假设 $order_data 包含 viewer_info 数组
|
||||
|
||||
// 1. 获取观影人信息数组
|
||||
$viewer_info = $order_data['viewer_info'] ?? [];
|
||||
|
||||
// 2. 遍历生成票据
|
||||
foreach ($seats as $index => $seat) {
|
||||
// 阶段1:所有票据使用第一个观影人信息(数组只有1个元素)
|
||||
// 未来扩展:根据 index 匹配对应的观影人
|
||||
$viewer = !empty($viewer_info[0]) ? $viewer_info[0] : [];
|
||||
|
||||
// 如果未来支持 require_viewer_per_seat=1,可以这样取:
|
||||
// $viewer = !empty($viewer_info[$index]) ? $viewer_info[$index] : $viewer_info[0];
|
||||
|
||||
$ticket_data = [
|
||||
// ... 现有字段
|
||||
'real_name' => $viewer['name'] ?? '',
|
||||
'phone' => $viewer['mobile'] ?? '',
|
||||
'id_card' => $viewer['idcard'] ?? '',
|
||||
// ... 其他字段
|
||||
];
|
||||
|
||||
// 插入票据
|
||||
Db::name('vr_tickets')->insert($ticket_data);
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 阶段1:`$viewer_info[0]` 复制到所有票据
|
||||
- 未来扩展:根据 `$index` 匹配不同观影人,无需修改数据结构
|
||||
|
||||
#### 步骤 3:数据传递机制
|
||||
|
||||
**关键问题**:如何将 `viewer_info` 从 `BuyService::OrderInsert` 传递到 `TicketService::issueTicket`?
|
||||
|
||||
**方案 A**:通过订单扩展数据字段传递
|
||||
- 将 `viewer_info` 存入 `vrt_order.extend_data` JSON 字段
|
||||
- `issueTicket` 从订单记录中读取
|
||||
|
||||
**方案 B**:通过插件 Hook 传递
|
||||
- 在 `OrderInsert` 触发插件 Hook 时,将 `viewer_info` 放入事件参数
|
||||
- 需要检查 ShopXO 的 Hook 机制是否支持
|
||||
|
||||
**推荐**:方案 A 更稳定,不依赖 Hook 机制
|
||||
|
||||
### 验证方案
|
||||
|
||||
#### 手动测试步骤
|
||||
|
||||
1. **创建测试订单**
|
||||
```bash
|
||||
curl -X POST http://localhost/api/buy/add \
|
||||
-d "goods_data=..." \
|
||||
-d "payment_id=1" \
|
||||
-d "viewer_name=测试用户" \
|
||||
-d "viewer_mobile=13800138000"
|
||||
```
|
||||
|
||||
2. **检查票据表数据**
|
||||
```sql
|
||||
SELECT id, ticket_code, real_name, phone
|
||||
FROM vrt_vr_tickets
|
||||
WHERE order_id = [刚创建的订单ID];
|
||||
```
|
||||
|
||||
3. **验证必填校验**
|
||||
```bash
|
||||
# 测试不提供手机号
|
||||
curl -X POST http://localhost/api/buy/add \
|
||||
-d "goods_data=..." \
|
||||
-d "payment_id=1"
|
||||
# 预期:如果商品要求观影人信息,应返回错误
|
||||
```
|
||||
|
||||
4. **验证手机号格式**
|
||||
```bash
|
||||
# 测试错误格式手机号
|
||||
curl -X POST http://localhost/api/buy/add \
|
||||
-d "viewer_mobile=12345"
|
||||
# 预期:返回"手机号格式不正确"错误
|
||||
```
|
||||
|
||||
### 前端集成指南
|
||||
|
||||
#### 订单确认页改动
|
||||
|
||||
**文件**:`goods-vr-ticket-order.vue`(前端 UniApp)
|
||||
|
||||
**改动点**:
|
||||
1. 在座位信息卡片和购票须知之间,插入观影人表单卡片
|
||||
2. 表单字段根据商品配置动态展示
|
||||
3. 提交订单时,将观影人信息组装为数组格式
|
||||
|
||||
**示例代码**:
|
||||
```vue
|
||||
<view class="section-card viewer-card" v-if="viewerConfig.require_viewer">
|
||||
<view class="section-title">观影人信息</view>
|
||||
|
||||
<input
|
||||
v-if="viewerConfig.require_viewer_name"
|
||||
v-model="viewerName"
|
||||
placeholder="请输入姓名"
|
||||
:class="{'required': viewerConfig.require_viewer_name}"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="viewerConfig.require_viewer_mobile"
|
||||
v-model="viewerMobile"
|
||||
placeholder="请输入手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
:class="{'required': viewerConfig.require_viewer_mobile}"
|
||||
/>
|
||||
|
||||
<!-- 阶段1暂不展示身份证号 -->
|
||||
<!-- <input v-if="viewerConfig.require_viewer_idcard" v-model="viewerIdcard" placeholder="请输入身份证号" /> -->
|
||||
|
||||
<!-- 微信小程序快速获取手机号 -->
|
||||
<button
|
||||
v-if="viewerConfig.require_viewer_mobile"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
class="quick-fill-btn"
|
||||
>
|
||||
快速填写手机号
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
viewerName: '',
|
||||
viewerMobile: '',
|
||||
viewerIdcard: '',
|
||||
viewerConfig: {
|
||||
require_viewer: 0,
|
||||
require_viewer_per_seat: 0,
|
||||
require_viewer_mobile: 0,
|
||||
require_viewer_name: 0,
|
||||
require_viewer_idcard: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 从商品详情获取观影人配置
|
||||
this.viewerConfig = this.goodsInfo.vr_ticket_config || {};
|
||||
|
||||
// 自动填充:从用户信息读取手机号
|
||||
if (this.userInfo.mobile) {
|
||||
this.viewerMobile = this.userInfo.mobile;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 微信小程序快速获取手机号
|
||||
onGetPhoneNumber(e) {
|
||||
if (e.detail.code) {
|
||||
// 调用后端接口解密手机号
|
||||
uni.request({
|
||||
url: '/api/wechat/decrypt_phone',
|
||||
data: { code: e.detail.code },
|
||||
success: (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.viewerMobile = res.data.data.phone;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
submitOrder() {
|
||||
// 阶段1:组装单个观影人数组
|
||||
const viewerData = [{
|
||||
name: this.viewerName,
|
||||
mobile: this.viewerMobile,
|
||||
idcard: this.viewerIdcard
|
||||
}];
|
||||
|
||||
const params = {
|
||||
goods_data: this.goodsData,
|
||||
payment_id: this.paymentId,
|
||||
viewer_data: viewerData // 数组结构
|
||||
};
|
||||
|
||||
// 调用订单提交接口
|
||||
uni.request({
|
||||
url: '/api/buy/add',
|
||||
method: 'POST',
|
||||
data: params,
|
||||
success: (res) => {
|
||||
// 处理结果
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.viewer-card .required::after {
|
||||
content: '*';
|
||||
color: red;
|
||||
}
|
||||
.quick-fill-btn {
|
||||
margin-top: 10px;
|
||||
background: #07c160;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 阶段1:`viewer_data` 数组固定长度为 1
|
||||
- 未来扩展:当 `require_viewer_per_seat=1` 时,循环生成多个观影人表单
|
||||
|
||||
#### 自动填充优化(可选)
|
||||
|
||||
如果用户账号有手机号,可以自动填充:
|
||||
```javascript
|
||||
// 页面加载时
|
||||
onLoad() {
|
||||
if (this.userInfo.mobile) {
|
||||
this.viewerMobile = this.userInfo.mobile;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 开发工作量评估
|
||||
|
||||
#### 后端改动(约 3-4 小时)
|
||||
- ✅ 后台商品配置界面:1 小时
|
||||
- ✅ 商品详情接口返回配置:30 分钟
|
||||
- ✅ 修改 `BuyService::OrderInsert`:1 小时
|
||||
- ✅ 修改 `TicketService::issueTicket`:30 分钟
|
||||
- ✅ 实现数据传递机制:30 分钟
|
||||
- ✅ 手动测试验证:1 小时
|
||||
|
||||
#### 前端改动(约 2-3 小时)
|
||||
- ✅ 读取商品配置并展示观影人表单:1.5 小时
|
||||
- ✅ 根据配置动态校验必填字段:30 分钟
|
||||
- ✅ 集成测试:1 小时
|
||||
|
||||
**总计**:5-7 小时
|
||||
|
||||
### 实施优先级
|
||||
|
||||
#### P0(核心功能 - 本次实施)
|
||||
1. ✅ **步骤 0**:后台商品配置界面(必须先做)
|
||||
2. ✅ **步骤 1**:订单提交接口校验逻辑
|
||||
3. ✅ **步骤 2**:票务出票流程集成
|
||||
4. ✅ **步骤 3**:数据传递机制(方案 A:订单扩展字段)
|
||||
5. ✅ **前端集成**:订单确认页观影人表单
|
||||
|
||||
#### P1(体验优化 - 后续迭代)
|
||||
- 手机号自动填充(从用户账号读取)
|
||||
- 商品详情接口返回观影人配置
|
||||
- 前端根据配置动态展示表单
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2 实现方案(TODO)
|
||||
|
||||
### 功能范围
|
||||
|
||||
1. **用户观影人管理页**
|
||||
- 观影人列表展示
|
||||
- 新增观影人
|
||||
- 编辑观影人
|
||||
- 删除观影人
|
||||
- 设置默认观影人
|
||||
|
||||
2. **订单确认页集成**
|
||||
- 快速选择历史观影人
|
||||
- 自动填充观影人信息
|
||||
- 优先展示默认观影人
|
||||
|
||||
3. **数据库设计**
|
||||
- 创建 `vrt_user_viewer` 表
|
||||
- 实现观影人 CRUD Service
|
||||
- 实现观影人 API 接口
|
||||
|
||||
### 开发工作量评估(TODO)
|
||||
|
||||
- 后端:6-8 小时
|
||||
- 前端:6-8 小时
|
||||
- 测试:2-3 小时
|
||||
- **总计**:14-19 小时
|
||||
|
||||
### 阶段 2 触发条件
|
||||
|
||||
当以下任一条件满足时,考虑实施阶段 2:
|
||||
1. 用户反馈每次填写观影人信息太麻烦
|
||||
2. 运营数据显示用户重复购票比例较高
|
||||
3. 产品需要硬实名认证功能(需要保存身份证号)
|
||||
|
||||
---
|
||||
|
||||
## 关键决策记录
|
||||
|
||||
| 决策点 | 阶段 1 方案 | 阶段 2 方案(TODO) |
|
||||
|--------|-------------|---------------------|
|
||||
| 数据存储 | 直接写入票据表 | 新增用户观影人表 + 票据表 |
|
||||
| 多票多人 | 一个订单一个观影人 | 支持每个座位独立观影人 |
|
||||
| 自动填充 | 前端从用户信息读取 | 后端返回历史观影人列表 |
|
||||
| 姓名字段 | 可选(待确认) | 必填 |
|
||||
| 身份证号 | 不使用 | 预留/必填(取决于实名要求) |
|
||||
|
||||
---
|
||||
|
||||
## 下一步行动
|
||||
|
||||
1. **确认姓名字段**:必填还是可选?
|
||||
2. **确认多票多人**:一个订单一个观影人,还是每个座位独立?
|
||||
3. **开始实施阶段 1**:
|
||||
- 修改 `BuyService::OrderInsert`
|
||||
- 修改 `TicketService::issueTicket`
|
||||
- 前端提供观影人表单
|
||||
|
||||
等待您的确认后,我立即开始实施阶段 1。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v2.0(修订版)
|
||||
**创建时间**:2026-06-24
|
||||
**修订原因**:根据用户反馈,分阶段实施,优先完成核心功能
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
# 观影人信息管理功能 - 实施总结
|
||||
|
||||
## ✅ 已完成的工作
|
||||
|
||||
### 1. 插件代码修改(纯插件实现)
|
||||
|
||||
#### 修改文件清单
|
||||
|
||||
**文件 1**: [Hook.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/.worktrees/FrontendDeveloper/shopxo/app/plugins/vr_ticket/Hook.php)
|
||||
- **新增方法**: `ValidateViewerData()` (第397-496行)
|
||||
- 从商品 `extends['vr_ticket']` 读取观影人配置
|
||||
- 校验 `viewer_data` 数组格式和长度
|
||||
- 根据配置校验手机号、姓名、身份证号(支持独立开关)
|
||||
- 将观影人信息注入 `$params['extension_data']['viewer_info']`
|
||||
- **修改方法**: `BuyCheck()` (第291-395行)
|
||||
- 在停售时间校验后增加观影人校验调用
|
||||
- 校验失败则阻止订单提交
|
||||
|
||||
**文件 2**: [TicketService.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/.worktrees/FrontendDeveloper/shopxo/app/plugins/vr_ticket/service/TicketService.php)
|
||||
- **修改方法**: `issueTicket()` (第222-249行)
|
||||
- 从 `order['extension_data']['viewer_info']` 读取观影人数组
|
||||
- 阶段1:所有票使用第一个观影人信息
|
||||
- 保持向后兼容旧格式 `attendee`
|
||||
- 支持字段映射:`name`→`real_name`, `mobile`→`phone`, `idcard`→`id_card`
|
||||
|
||||
#### 数据流程
|
||||
|
||||
```
|
||||
前端提交订单
|
||||
↓ (viewer_data 数组参数)
|
||||
Hook::BuyCheck (plugins_service_buy_order_insert_begin)
|
||||
↓ (校验成功)
|
||||
Hook::ValidateViewerData
|
||||
↓ (注入 extension_data)
|
||||
BuyService::OrderInsert
|
||||
↓ (创建订单,存储 extension_data 到数据库)
|
||||
订单支付成功
|
||||
↓ (plugins_service_order_pay_success_handle_end)
|
||||
TicketService::onOrderPaid
|
||||
↓
|
||||
TicketService::issueTicket
|
||||
↓ (读取 extension_data.viewer_info[0])
|
||||
更新 vrt_tickets 表 (real_name, phone, id_card)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 配置结构
|
||||
|
||||
### 商品扩展配置 (存储在 `sxo_goods.extends` JSON 字段)
|
||||
|
||||
```json
|
||||
{
|
||||
"vr_ticket": {
|
||||
"require_viewer": 1, // 总开关:是否需要观影人信息
|
||||
"require_viewer_per_seat": 0, // 多座位模式(阶段1=0,未来=1)
|
||||
"require_viewer_mobile": 1, // 必填手机号
|
||||
"require_viewer_name": 0, // 必填姓名(可选)
|
||||
"require_viewer_idcard": 0 // 必填身份证号(预留)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 订单扩展数据 (存储在 `sxo_order.extension_data` JSON 字段)
|
||||
|
||||
```json
|
||||
{
|
||||
"viewer_info": [
|
||||
{
|
||||
"name": "张三",
|
||||
"mobile": "13800138000",
|
||||
"idcard": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试步骤
|
||||
|
||||
### 前置准备
|
||||
|
||||
1. **配置商品观影人要求**
|
||||
|
||||
手动更新商品的 `extends` 字段(临时方法,等后台界面完成后使用界面配置):
|
||||
|
||||
```sql
|
||||
UPDATE vrt_goods
|
||||
SET extends = JSON_SET(
|
||||
COALESCE(extends, '{}'),
|
||||
'$.vr_ticket.require_viewer', 1,
|
||||
'$.vr_ticket.require_viewer_mobile', 1,
|
||||
'$.vr_ticket.require_viewer_name', 0
|
||||
)
|
||||
WHERE id = <商品ID>;
|
||||
```
|
||||
|
||||
### 测试用例 1:必填手机号校验
|
||||
|
||||
**前端请求**:
|
||||
```bash
|
||||
curl -X POST 'http://localhost/api/buy/add' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"goods_data": "...",
|
||||
"payment_id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**预期结果**:返回错误 `{"code": -1, "msg": "请填写观影人信息"}`
|
||||
|
||||
### 测试用例 2:手机号格式校验
|
||||
|
||||
**前端请求**:
|
||||
```bash
|
||||
curl -X POST 'http://localhost/api/buy/add' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"goods_data": "...",
|
||||
"payment_id": 1,
|
||||
"viewer_data": [
|
||||
{"name": "张三", "mobile": "12345", "idcard": ""}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**预期结果**:返回错误 `{"code": -1, "msg": "手机号格式不正确"}`
|
||||
|
||||
### 测试用例 3:正常提交订单
|
||||
|
||||
**前端请求**:
|
||||
```bash
|
||||
curl -X POST 'http://localhost/api/buy/add' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"goods_data": "...",
|
||||
"payment_id": 1,
|
||||
"viewer_data": [
|
||||
{"name": "张三", "mobile": "13800138000", "idcard": ""}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
1. 订单创建成功
|
||||
2. 查询订单表:`extension_data` 包含 `viewer_info`
|
||||
3. 订单支付后,查询票据表:`real_name="张三"`, `phone="13800138000"`
|
||||
|
||||
```sql
|
||||
-- 查询订单扩展数据
|
||||
SELECT id, order_no, extension_data
|
||||
FROM vrt_order
|
||||
WHERE id = <订单ID>;
|
||||
|
||||
-- 查询票据观影人信息
|
||||
SELECT id, ticket_code, real_name, phone, id_card
|
||||
FROM vrt_vr_tickets
|
||||
WHERE order_id = <订单ID>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 待完成工作
|
||||
|
||||
### P0 - 后台配置界面
|
||||
|
||||
**目标**:在插件后台提供商品观影人配置管理界面
|
||||
|
||||
**实施方案**:
|
||||
1. 在 `admin/Admin.php` 中添加配置管理方法
|
||||
2. 创建后台配置页面模板
|
||||
3. 提供商品列表,支持批量配置观影人要求
|
||||
|
||||
**预计工作量**:1-2 小时
|
||||
|
||||
### P1 - 前端集成
|
||||
|
||||
**目标**:订单确认页展示观影人表单
|
||||
|
||||
**文件**:`vr-shopxo-uniapp/pages/goods-vr-ticket/goods-vr-ticket-order.vue`
|
||||
|
||||
**实施要点**:
|
||||
1. 从商品详情读取 `vr_ticket_config`
|
||||
2. 动态展示观影人表单(根据配置显示必填字段)
|
||||
3. 提交订单时,将观影人信息组装为 `viewer_data` 数组
|
||||
4. 集成微信小程序 `getPhoneNumber` 快速授权
|
||||
|
||||
**预计工作量**:2-3 小时
|
||||
|
||||
参考实现:[implementation_plan.md](file:///Users/bigemon/.gemini/antigravity-ide/brain/bb454876-da6d-4c57-8e48-ce7de6be1116/implementation_plan.md#L409-L522)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 关键设计决策
|
||||
|
||||
### 1. 纯插件实现
|
||||
|
||||
✅ **不修改 ShopXO 核心代码**
|
||||
- 通过 Hook `plugins_service_buy_order_insert_begin` 介入订单流程
|
||||
- 升级 ShopXO 不受影响
|
||||
|
||||
### 2. 数组结构的前瞻性
|
||||
|
||||
✅ **支持未来扩展到多人多票**
|
||||
|
||||
**阶段1**(当前):
|
||||
```json
|
||||
"viewer_data": [{"name": "张三", "mobile": "138..."}] // 长度=1
|
||||
```
|
||||
|
||||
**阶段2**(未来):
|
||||
```json
|
||||
"viewer_data": [
|
||||
{"name": "张三", "mobile": "138..."}, // 座位1
|
||||
{"name": "李四", "mobile": "139..."} // 座位2
|
||||
]
|
||||
```
|
||||
|
||||
代码无需修改接口,只需:
|
||||
- `ValidateViewerData`: 校验数组长度 = 座位数量
|
||||
- `issueTicket`: 根据座位索引匹配观影人
|
||||
|
||||
### 3. 灵活的字段控制
|
||||
|
||||
✅ **5 个独立开关,商家自定义配置**
|
||||
|
||||
| 开关 | 说明 | 阶段1状态 |
|
||||
|------|------|-----------|
|
||||
| `require_viewer` | 总开关 | 可用 |
|
||||
| `require_viewer_per_seat` | 多座位模式 | 暂不启用 |
|
||||
| `require_viewer_mobile` | 必填手机号 | 可用(核心字段)|
|
||||
| `require_viewer_name` | 必填姓名 | 可用 |
|
||||
| `require_viewer_idcard` | 必填身份证号 | 预留 |
|
||||
|
||||
### 4. 向后兼容
|
||||
|
||||
✅ **保持对旧数据格式的支持**
|
||||
|
||||
`issueTicket` 同时支持:
|
||||
- 新格式:`extension_data.viewer_info` (数组)
|
||||
- 旧格式:`extension_data.attendee` (对象)
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
### 表前缀
|
||||
|
||||
代码中使用 `BaseService::table('tickets')` 或 `Db::name('vr_tickets')` 而不是硬编码 `vrt_vr_tickets`,因为 ShopXO 会自动添加配置的表前缀。
|
||||
|
||||
### 日志调试
|
||||
|
||||
观影人校验过程已添加日志:
|
||||
```php
|
||||
BaseService::log('ValidateViewerData: success', [
|
||||
'viewer_count' => count($viewer_data),
|
||||
'viewer_data' => $viewer_data
|
||||
], 'info');
|
||||
```
|
||||
|
||||
查看日志文件(需确认实际路径):
|
||||
```bash
|
||||
tail -f shopxo/runtime/log/*.log | grep ValidateViewerData
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 部署清单
|
||||
|
||||
### 1. 代码部署
|
||||
|
||||
将修改后的文件部署到生产环境:
|
||||
- `shopxo/app/plugins/vr_ticket/Hook.php`
|
||||
- `shopxo/app/plugins/vr_ticket/service/TicketService.php`
|
||||
|
||||
### 2. 配置示例商品
|
||||
|
||||
为测试商品配置观影人要求(使用上述 SQL)
|
||||
|
||||
### 3. 前端更新
|
||||
|
||||
等待前端团队完成订单确认页的观影人表单集成
|
||||
|
||||
---
|
||||
|
||||
**完成时间**:2026-06-24
|
||||
**实施人员**:Claude Code (Backend)
|
||||
**文档版本**:v1.0
|
||||
|
|
@ -409,6 +409,18 @@ export default {
|
|||
onLoad(params) {
|
||||
const app = getApp();
|
||||
this.app = app;
|
||||
|
||||
// 如果是通过 base64 传递的单一 data 参数,先解包
|
||||
if (params && params.data) {
|
||||
try {
|
||||
const base64 = require('@/common/js/lib/base64.js');
|
||||
const decodedStr = base64.decode(decodeURIComponent(params.data));
|
||||
const decodedParams = JSON.parse(decodedStr);
|
||||
params = Object.assign({}, decodedParams, params);
|
||||
} catch (e) {
|
||||
console.error('[Order Confirm] Unpack params.data failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 主题
|
||||
this.theme_view = app.globalData.get_theme_value_view();
|
||||
|
|
@ -427,41 +439,23 @@ export default {
|
|||
|
||||
// 解析座位数据
|
||||
if (params.seats) {
|
||||
try {
|
||||
this.confirmedSeats = JSON.parse(decodeURIComponent(params.seats));
|
||||
} catch (e) {
|
||||
console.error('[Order Confirm] Parse seats failed:', e);
|
||||
this.confirmedSeats = [];
|
||||
}
|
||||
this.confirmedSeats = this.safeParseJson(params.seats) || [];
|
||||
}
|
||||
|
||||
// 解析选中的规格
|
||||
if (params.selection) {
|
||||
try {
|
||||
this.currentSelection = JSON.parse(decodeURIComponent(params.selection));
|
||||
} catch (e) {
|
||||
console.error('[Order Confirm] Parse selection failed:', e);
|
||||
this.currentSelection = {};
|
||||
}
|
||||
this.currentSelection = this.safeParseJson(params.selection) || {};
|
||||
}
|
||||
|
||||
// 解析须知数据
|
||||
if (params.buyNotices) {
|
||||
try {
|
||||
this.buyNotices = JSON.parse(decodeURIComponent(params.buyNotices));
|
||||
} catch (e) {
|
||||
this.buyNotices = this.defaultBuyNotices;
|
||||
}
|
||||
this.buyNotices = this.safeParseJson(params.buyNotices) || this.defaultBuyNotices;
|
||||
} else {
|
||||
this.buyNotices = this.defaultBuyNotices;
|
||||
}
|
||||
|
||||
if (params.services) {
|
||||
try {
|
||||
this.services = JSON.parse(decodeURIComponent(params.services));
|
||||
} catch (e) {
|
||||
this.services = this.defaultServices;
|
||||
}
|
||||
this.services = this.safeParseJson(params.services) || this.defaultServices;
|
||||
} else {
|
||||
this.services = this.defaultServices;
|
||||
}
|
||||
|
|
@ -473,28 +467,23 @@ export default {
|
|||
|
||||
// 解析场次日期时间
|
||||
if (params.sessionDatetime) {
|
||||
this.sessionDatetime = decodeURIComponent(params.sessionDatetime);
|
||||
this.sessionDatetime = this.safeDecodeURIComponent(params.sessionDatetime);
|
||||
}
|
||||
|
||||
// 解析海报URL
|
||||
if (params.posterUrl) {
|
||||
this.posterUrl = decodeURIComponent(params.posterUrl);
|
||||
this.posterUrl = this.safeDecodeURIComponent(params.posterUrl);
|
||||
}
|
||||
|
||||
// 解析场馆地址
|
||||
if (params.venueAddress) {
|
||||
this.venueAddress = decodeURIComponent(params.venueAddress);
|
||||
this.venueAddress = this.safeDecodeURIComponent(params.venueAddress);
|
||||
}
|
||||
|
||||
// 解析观影人配置
|
||||
if (params.viewerConfig) {
|
||||
try {
|
||||
this.viewerConfig = JSON.parse(decodeURIComponent(params.viewerConfig));
|
||||
console.log('[Order] viewerConfig loaded:', this.viewerConfig);
|
||||
} catch (e) {
|
||||
console.error('[Order] Failed to parse viewerConfig:', e);
|
||||
this.viewerConfig = { require_viewer: false };
|
||||
}
|
||||
this.viewerConfig = this.safeParseJson(params.viewerConfig) || { require_viewer: false };
|
||||
console.log('[Order] viewerConfig loaded:', this.viewerConfig);
|
||||
}
|
||||
|
||||
// 自动填充观影人信息
|
||||
|
|
@ -987,6 +976,32 @@ export default {
|
|||
uni.navigateBack();
|
||||
},
|
||||
|
||||
// 安全解析 JSON
|
||||
safeParseJson(val) {
|
||||
if (!val) return null;
|
||||
if (typeof val === 'object') return val;
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(val));
|
||||
} catch (e2) {
|
||||
console.error('[Order Confirm] safeParseJson failed for:', val, e2);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 安全解码 URI Component
|
||||
safeDecodeURIComponent(val) {
|
||||
if (!val) return '';
|
||||
try {
|
||||
return decodeURIComponent(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
},
|
||||
|
||||
// 设置数据的兼容方法
|
||||
setData(obj) {
|
||||
Object.keys(obj).forEach(key => {
|
||||
|
|
|
|||
|
|
@ -608,30 +608,28 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
// 组装跳转参数
|
||||
const params = {
|
||||
// 组装跳转参数,使用 base64 封装以避免 URL 刷新丢失或格式解析错误
|
||||
const dataParams = {
|
||||
goodsId: this.goodsId,
|
||||
seats: encodeURIComponent(JSON.stringify(this.confirmedSeats)),
|
||||
selection: encodeURIComponent(JSON.stringify(this.currentSelection)),
|
||||
buyNotices: encodeURIComponent(JSON.stringify(this.buyNotices)),
|
||||
services: encodeURIComponent(JSON.stringify(this.services)),
|
||||
seats: JSON.stringify(this.confirmedSeats),
|
||||
selection: JSON.stringify(this.currentSelection),
|
||||
buyNotices: JSON.stringify(this.buyNotices),
|
||||
services: JSON.stringify(this.services),
|
||||
batchExpire: this.goodsData.batch_number_expire || '',
|
||||
sessionDatetime: sessionDatetime,
|
||||
posterUrl: posterImg,
|
||||
venueAddress: this.currentVenue?.address || '',
|
||||
viewerConfig: viewerConfig ? encodeURIComponent(JSON.stringify(viewerConfig)) : ''
|
||||
viewerConfig: viewerConfig ? JSON.stringify(viewerConfig) : ''
|
||||
};
|
||||
|
||||
// 拼接 URL 参数
|
||||
let url = '/pages/goods-vr-ticket-order/goods-vr-ticket-order?';
|
||||
Object.keys(params).forEach(key => {
|
||||
url += `${key}=${params[key]}&`;
|
||||
});
|
||||
const base64 = require('@/common/js/lib/base64.js');
|
||||
const dataStr = encodeURIComponent(base64.encode(JSON.stringify(dataParams)));
|
||||
const url = `/pages/goods-vr-ticket-order/goods-vr-ticket-order?data=${dataStr}`;
|
||||
|
||||
console.log('[VR Ticket] Navigate to order confirm:', url);
|
||||
|
||||
uni.navigateTo({
|
||||
url: url.slice(0, -1) // 移除末尾的 &
|
||||
url: url
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue