vr-shopxo-plugin/docs/16_VR_TICKET_FIELD_VALIDATI...

9.4 KiB
Raw Blame History

VR Ticket 票务商品字段校验方案

状态: 已实施 版本: 1.0.0 实施日期: 2026-05-18 负责人: 西莉雅、大头


目录

  1. 需求背景
  2. 字段约束规则
  3. 业务影响分析
  4. 实施记录
  5. 前端适配
  6. FAQ

一、需求背景

当前票务商品存在以下隐患:

  1. 用户未填 batch_number_expire演出日期导致 session_meta 为空,前端无法展示倒计时和场次禁用逻辑
  2. 用户未填 coding商品编号导致 peer_goods 无法关联
  3. 未对 (coding, batch_number_expire) 组合做唯一性约束,同一演出的不同日期商品可能冲突

二、字段约束规则

2.1 字段说明

字段 数据库列 用途 约束
商品编号 goods.coding 同演出多日期关联peer_goods 非强制,但有编号才能关联同演出商品
演出日期 goods.batch_number_expire 演出当天日期Unix 时间戳) 强制,票务商品必须填写

2.2 唯一性约束

(coding, batch_number_expire) 组合唯一,由应用层保存钩子校验,原生数据库无此关联索引。

含义:

  • 同一 coding同一演出不同 batch_number_expire = 不同日期场次(正常)
  • 同一 coding 下,不能有两个相同的 batch_number_expire禁止
  • coding 为空时batch_number_expire 也不能与其他空 coding 的记录重复(禁止,因为会破坏日期切分逻辑)

注意:该唯一性由 AdminGoodsSaveHandle 钩子在保存时校验,数据库层面不创建联合唯一索引。


三、业务影响分析

3.1 未填写 batch_number_expire 的后果

  • tree API 返回的 peer_goods.date = "",前端无法展示日期切换控件
  • tree API 返回的 session_meta 为空或 date="",前端的场次选择卡无倒计时、无过期判断
  • 后端 BuyCheck 钩子无法验证停售时间

3.2 未填写 coding 的后果

  • peer_goods 返回空,前端不展示多日期切换控件
  • 不影响下单

3.3 两者均未填的后果

  • 多条票务商品会共享 (coding='', batch_number_expire=0),违反唯一性规则
  • 保存时被 AdminGoodsSaveHandle 钩子拦截,报错提示"该日期已存在其他票务商品"

四、实施记录

实施日期: 2026-05-18 所有阶段均已完成。 以下为各阶段的实现细节与代码位置。

Phase 1 :后台配置界面动态必填控制

文件: shopxo/app/plugins/vr_ticket/hook/AdminGoodsSave.php (L283-340)

实现方式: Vue watch + DOM 操作,无侵入式动态注入

核心函数: applyTicketRequired(required)

功能清单:

  • 监听 isTicket 复选框:勾选时为 batch_number_expirecoding 注入 required 属性
  • 视觉反馈:标签后追加红色 * 标记CSS class: vr-ticket-field-star
  • 占位提示input placeholder 替换为「票务商品「演出日期」必填」等友好提示
  • 取消勾选时彻底清理(仅移除插件添加的属性,保护后台模板原生 required
  • data-vr-required-src 标记区分插件注入 vs 模板预置

关键代码片段:

fields.forEach(({ name, label }) => {
    const input = document.querySelector('input[name="' + name + '"]');
    if (!input) return;
    if (required) {
        if (!input.hasAttribute('required')) {
            input.setAttribute('required', '');
            input.setAttribute('data-vr-required-src', '1');
        }
        // 添加红 * 视觉提示
        // 修改 placeholder 为票务提示
    } else {
        // 仅移除插件添加的 required不影响后台模板预置的 required
        if (input.getAttribute('data-vr-required-src') === '1') {
            input.removeAttribute('required');
        }
        // 恢复原生 placeholder
    }
});

Phase 2 :后端表单校验(保存钩子)

文件: shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php (L60-95)

触发时机: plugins_service_goods_save_thing_end 钩子事务内goods 表已落库)

校验项:

# 校验 条件 错误消息
1 演出日期必填 batch_number_expire <= 0 「票务商品必须设置演出日期(批号有效期),请填写后重新保存」
2 组合唯一性 (coding, batch_number_expire) 重复 「该商品编号「{coding}」在此演出日期已存在商品「{title}」」
2b 组合唯一性coding 为空) 空 coding + 相同日期重复 「该演出日期已存在其他未设置编号的票务商品「{title}」」

唯一性校验 SQL 逻辑:

SELECT id, title, coding FROM `goods`
WHERE batch_number_expire = {batch_number_expire}
  AND (coding = {coding} OR (coding = '' AND {coding} = ''))
  AND is_delete_time = 0
  AND id != {current_goods_id}
  AND vr_goods_config != ''
LIMIT 1

设计决策: 唯一性约束在应用层实施,不在数据库建联合索引,以保持与 ShopXO 原生结构的兼容性。

Phase 3 BuyCheck 下单校验

文件: shopxo/app/plugins/vr_ticket/Hook.php (L264-368)

触发时机: plugins_service_buy_order_insert_begin 钩子(订单创建前)

两层校验:

校验层 条件 错误消息 行为
数据完整性 batch_number_expire <= 0 「「{title}」未设置演出日期,暂时无法购买」 拒绝下单
停售时效 now >= batch_expire_ts 「该场次({start} {end}距开场已不足5分钟已停止售票」 拒绝下单

关键实现细节:

  • 批量查询优化:一次性查询所有 goods_idvr_goods_config + batch_number_expire
  • batch_expire_ts 从 SKU 的 extends JSON 字段读取(由 SeatSkuService::BatchGenerate 写入)
  • 非票务商品:完全跳过,零性能影响
  • extends 缺失或为空:跳过停售校验(保守放过策略)

逻辑隔离: 所有票务专属逻辑由 vr_goods_config 非空判断保护,普通商品完全不受影响。

五、前端适配

仅票务商品适用以下规则。普通商品不受影响。

当用户未填写相关字段时,前端需要感知并降级:

字段 填写情况 前端表现
batch_number_expire 场次选择卡无倒计时无停售禁用逻辑tree API 的 session_meta 为空
coding 不显示多日期切换控件tree API 的 peer_goods 为空数组
两者均空 冲突 保存时被 AdminGoodsSaveHandle 拦截BuyCheck 拒绝下单

5.1 后台管理界面(已实施)

后台商品编辑页中当「VR Ticket」复选框勾选时batch_number_expire(演出日期)和 coding(商品编号)将动态变为必填,标签旁显示红色星号,输入框 placeholder 提示「票务商品「XX」必填」。

5.2 前端UniApp

前端可通过 tree API 的 session_metapeer_goods 的返回情况来判断数据完整性:

// 检测数据完整性
function checkDataIntegrity(apiData) {
    const warnings = [];
    
    // 检查是否有场次元数据
    if (!apiData.session_meta || apiData.session_meta.length === 0) {
        warnings.push('该商品未设置有效的演出日期,无法展示场次信息');
    }
    
    // 检查是否有同场次关联商品
    if (!apiData.peer_goods || apiData.peer_goods.length === 0) {
        // coding 未填,无关联商品,但这不影响基本选座流程
    }
    
    return warnings;
}

六、FAQ

Q: 普通商品(非票务)会受影响吗?

不会。 票务相关逻辑的判断入口统一为:

$vrConfig = Db::name('Goods')->where('id', $goodsId)->value('vr_goods_config');
if (empty($vrConfig)) {
    // 普通商品,跳过所有票务校验和逻辑
    return;
}
  • Phase 1后台表单仅票务商品可见演出日期/商品编号配置区块
  • Phase 2保存钩子仅票务商品执行必填和唯一性校验
  • Phase 3BuyCheck仅票务商品执行停售时间校验

普通商品的 batch_number_expirecoding 字段不受任何影响,可以正常使用或留空。

Q: 为什么不在数据库层面建唯一索引?

保持与 ShopXO 原生数据库结构的兼容性。(coding, batch_number_expire) 的唯一性仅在票务商品(vr_goods_config != '')范围内生效,数据库唯一索引无法表达这种条件约束。

Q: BuyCheck 的停售校验为什么是"不足5分钟"

batch_expire_ts = 演出开始时间戳 - 300秒5分钟,由 SeatSkuService::BatchGenerate() 在 SKU 生成时计算。这个5分钟缓冲给用户足够的支付和确认时间。

Q: 如果 coding 为空peer_goods 会怎样?

tree API 的 peer_goods 返回空数组 []。前端应据此隐藏多日期切换控件,这不影响基本选座和下单流程。


附录:相关代码文件

文件 作用
hook/AdminGoodsSave.php Phase 1 — 前端动态必填控件Vue + DOM
hook/AdminGoodsSaveHandle.php Phase 2 — 后端唯一性/必填校验
Hook.php Phase 3 — BuyCheck 下单拦截
service/SeatSkuService.php SKU 生成时写入 batch_expire_ts
goods/Goods.php (tree action) tree API 返回 session_meta / peer_goods

文档由 Antigravity 生成 · 2026-05-18