feat(vr_ticket): 票务商品 site_type 锁定为虚拟
票务商品的结算/拆单/加购流程强依赖虚拟商品类型(site_type=3)。 当商品设为票务(item_type=ticket)时,三层防御锁定 site_type: 1. UI 层(新增 AdminGoodsSaveForbid 钩子) 监听 plugins_service_goods_base_forbid_operate_data, 把 site_type 加入禁用列表,触发 saveinfo.html 第 228 行 disabled 渲染。 2. 服务端层(AdminGoodsSaveHandle) plugins_service_goods_save_handle 票务分支新增 $params['data']['site_type'] = 3,强制覆盖。 3. 前端 JS 层(AdminGoodsSave) 新增 syncSiteTypeLock(),watch(isTicket) 中调用, isTicket=true 强制 value=3 + disabled; isTicket=false 解除(仅移除插件添加的标记)。 仅在票务关闭(item_type=normal)后才允许修改 site_type。 风险等级:LOW(gitnexus detect_changes: 0 affected processes)main
parent
6fc5587145
commit
73e21da656
|
|
@ -47,6 +47,9 @@
|
|||
"plugins_service_goods_save_thing_end": [
|
||||
"app\\plugins\\vr_ticket\\hook\\AdminGoodsSaveHandle"
|
||||
],
|
||||
"plugins_service_goods_base_forbid_operate_data": [
|
||||
"app\\plugins\\vr_ticket\\hook\\AdminGoodsSaveForbid"
|
||||
],
|
||||
"plugins_css_data": [
|
||||
"app\\plugins\\vr_ticket\\hook\\ViewGoodsCss"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -360,6 +360,50 @@ class AdminGoodsSave
|
|||
}
|
||||
};
|
||||
|
||||
// ── 商品类型 site_type 锁定控制(票务商品专用) ──
|
||||
// 票务商品的结算/拆单/加购流程强依赖"虚拟"商品类型(value=3)。
|
||||
// · isTicket=true → 强制 site_type=3(虚拟),并 disabled 该 select
|
||||
// · isTicket=false → 恢复可选,但不修改当前值(保留用户上次选择)
|
||||
// 注:解除 disabled 时仅移除本插件添加的标记,避免破坏其他钩子的禁用控制
|
||||
const SITE_TYPE_VIRTUAL_VALUE = 3; // 商品类型 = 虚拟
|
||||
const syncSiteTypeLock = (locked) => {
|
||||
const select = document.querySelector('select[name="site_type"]');
|
||||
if (!select) return;
|
||||
|
||||
if (locked) {
|
||||
// 强制选中"虚拟"
|
||||
select.value = String(SITE_TYPE_VIRTUAL_VALUE);
|
||||
// 标记并禁用(仅在尚未禁用时操作,避免覆盖其他钩子的禁用)
|
||||
if (!select.hasAttribute('data-vr-ticket-locked')) {
|
||||
select.setAttribute('data-vr-ticket-locked', '1');
|
||||
// 记录原始 disabled 状态以便恢复
|
||||
if (select.hasAttribute('disabled')) {
|
||||
select.setAttribute('data-vr-ticket-prev-disabled', '1');
|
||||
} else {
|
||||
select.removeAttribute('data-vr-ticket-prev-disabled');
|
||||
}
|
||||
select.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
// 触发 chosen 更新以反映新选中项
|
||||
if (window.$ && $.fn.chosen && $(select).data('chosen')) {
|
||||
$(select).trigger('chosen:updated');
|
||||
}
|
||||
} else {
|
||||
// 解除禁用(仅解除本插件添加的)
|
||||
if (select.getAttribute('data-vr-ticket-locked') === '1') {
|
||||
select.removeAttribute('data-vr-ticket-locked');
|
||||
if (!select.hasAttribute('data-vr-ticket-prev-disabled')) {
|
||||
select.removeAttribute('disabled');
|
||||
} else {
|
||||
select.removeAttribute('data-vr-ticket-prev-disabled');
|
||||
}
|
||||
}
|
||||
if (window.$ && $.fn.chosen && $(select).data('chosen')) {
|
||||
$(select).trigger('chosen:updated');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── 原生字段强制必填控制(票务商品专用) ──
|
||||
// 当 isTicket 勾选时:强制 batch_number_expire、coding、produce_region 为必填
|
||||
// 当 isTicket 取消时:恢复原始状态(仅移除插件添加的 required)
|
||||
|
|
@ -483,6 +527,8 @@ class AdminGoodsSave
|
|||
|
||||
watch(isTicket, (val) => {
|
||||
applyTicketRequired(val);
|
||||
// 票务商品时锁定 site_type 为"虚拟";取消时解锁
|
||||
syncSiteTypeLock(val);
|
||||
if (val) {
|
||||
// 票务商品模式:替换城市下拉选项
|
||||
replaceCityOptions();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
namespace app\plugins\vr_ticket\hook;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 后台商品保存页 — 字段禁用控制钩子
|
||||
*
|
||||
* 监听 ShopXO 的 `plugins_service_goods_base_forbid_operate_data` 钩子。
|
||||
* 当商品为票务商品(item_type='ticket')时,把 site_type(商品类型)字段
|
||||
* 加入禁用列表 `$data`,触发 saveinfo.html 第 228 行的 `disabled` 渲染。
|
||||
*
|
||||
* 为什么这样设计?
|
||||
* - 票务商品的结算流程强依赖"虚拟"商品类型(site_type=3),
|
||||
* 否则订单拆单、加入购物车逻辑会失效。
|
||||
* - ShopXO 已提供原生扩展点,无需修改公共模板。
|
||||
*
|
||||
* 配合 AdminGoodsSaveHandle.php 中的服务端强制锁定,构成"双层防御":
|
||||
* - UI 层(这里):禁用 select 控件
|
||||
* - 服务端层(AdminGoodsSaveHandle.php):即使绕过前端,也会强制 site_type=3
|
||||
*/
|
||||
class AdminGoodsSaveForbid
|
||||
{
|
||||
/**
|
||||
* ShopXO 钩子入口
|
||||
*
|
||||
* @param array $params 钩子参数,包含:
|
||||
* - hook_name : 钩子名称(应等于本钩子)
|
||||
* - goods_id : 商品 ID(0 = 新建)
|
||||
* - goods : 商品数据(含 item_type 字段)
|
||||
* - params : 输入参数
|
||||
* - data : 引用传递,禁用字段列表
|
||||
* @return void
|
||||
*/
|
||||
public function handle($params = [])
|
||||
{
|
||||
// 仅响应禁用字段控制钩子
|
||||
if (($params['hook_name'] ?? '') !== 'plugins_service_goods_base_forbid_operate_data') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据引用(禁用字段列表)
|
||||
if (!isset($params['data']) || !is_array($params['data'])) {
|
||||
return;
|
||||
}
|
||||
$data = &$params['data'];
|
||||
|
||||
// 判定是否为票务商品:优先用 $goods 数组,否则从 DB 查询
|
||||
$isTicket = false;
|
||||
$goodsId = intval($params['goods_id'] ?? 0);
|
||||
$goods = $params['goods'] ?? [];
|
||||
|
||||
if (!empty($goods) && isset($goods['item_type'])) {
|
||||
$isTicket = ($goods['item_type'] === 'ticket');
|
||||
} elseif ($goodsId > 0) {
|
||||
// 编辑场景下若 goods 未传完整,回查数据库(防御性兜底)
|
||||
$itemType = Db::name('Goods')->where('id', $goodsId)->value('item_type');
|
||||
$isTicket = ($itemType === 'ticket');
|
||||
}
|
||||
|
||||
// 票务商品 → 禁用 site_type(商品类型),强制锁定为"虚拟"
|
||||
if ($isTicket && !in_array('site_type', $data, true)) {
|
||||
$data[] = 'site_type';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,12 @@ class AdminGoodsSaveHandle
|
|||
$params['data']['item_type'] = 'ticket';
|
||||
$params['data']['is_exist_many_spec'] = 1;
|
||||
|
||||
// ── 服务端强制锁定 site_type ──
|
||||
// 票务商品的结算/拆单/加购流程强依赖"虚拟"商品类型(value=3)。
|
||||
// 即使前端绕过 disabled 控件或被恶意构造请求提交其他值,
|
||||
// 这里也强制覆盖为 3,确保数据完整性。
|
||||
$params['data']['site_type'] = 3;
|
||||
|
||||
$base64Config = $postParams['vr_goods_config_base64'] ?? '';
|
||||
if (!empty($base64Config)) {
|
||||
$jsonStr = base64_decode($base64Config);
|
||||
|
|
|
|||
Loading…
Reference in New Issue