vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php

236 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace app\plugins\vr_ticket\hook;
use think\facade\Db;
use app\plugins\vr_ticket\service\SeatSkuService;
class AdminGoodsSaveHandle
{
/**
* 商品保存钩子(同时响应 save_handle 和 save_thing_end 两个时机)
*/
public function handle($params = [])
{
$hookName = $params['hook_name'] ?? '';
// ──────────────────────────────────────────────────────
// 时机 1plugins_service_goods_save_handle事务前修改待保存数据
// - 把 vr_goods_config base64 解码写入 $data
// - 设置 item_type
// - 强制 is_exist_many_spec = 1
// ──────────────────────────────────────────────────────
if ($hookName === 'plugins_service_goods_save_handle') {
$postParams = $params['params'] ?? [];
if (isset($postParams['vr_is_ticket']) && $postParams['vr_is_ticket'] == 1) {
$params['data']['item_type'] = 'ticket';
$params['data']['is_exist_many_spec'] = 1;
$base64Config = $postParams['vr_goods_config_base64'] ?? '';
if (!empty($base64Config)) {
$jsonStr = base64_decode($base64Config);
if ($jsonStr !== false) {
$params['data']['vr_goods_config'] = $jsonStr;
}
}
} else {
$params['data']['item_type'] = 'normal';
$params['data']['vr_goods_config'] = '';
}
return ['code' => 0];
}
// ──────────────────────────────────────────────────────
// 时机 2plugins_service_goods_save_thing_end事务内goods 已落表)
// 关键:此时 GoodsSpecificationsInsert + GoodsSaveBaseUpdate
// 已经执行完毕(它们处理的是表单原生规格数据)。
//
// 对于票务商品,我们需要:
// a) 删除原生流程产生的所有 spec 数据(表单垃圾)
// b) 用 BatchGenerate 重新生成 VR 座位级 SKU
// c) 回写 GoodsSpecType.value让后台正确展示
// d) 重新计算 goods 表的 price/inventory
// ──────────────────────────────────────────────────────
if ($hookName === 'plugins_service_goods_save_thing_end') {
$data = $params['data'] ?? [];
$goodsId = $params['goods_id'] ?? 0;
if ($goodsId > 0 && ($data['item_type'] ?? '') === 'ticket') {
// ──────────────────────────────────────────────────────
// 票务商品字段校验Phase 2必填 + 唯一性)
// · batch_number_expire 必须 > 0演出日期必填
// · (coding, batch_number_expire) 组合唯一(应用层校验)
// ──────────────────────────────────────────────────────
$batchNumberExpire = intval($data['batch_number_expire'] ?? 0);
// 1. 演出日期必填校验
if ($batchNumberExpire <= 0) {
return ['code' => -1, 'msg' => '票务商品必须设置演出日期(批号有效期),请填写后重新保存'];
}
// 2. (coding, batch_number_expire) 唯一性校验
$coding = trim($data['coding'] ?? '');
$where = [
['batch_number_expire', '=', $batchNumberExpire],
['is_delete_time', '=', 0],
['id', '<>', $goodsId],
['vr_goods_config', '<>', ''], // 仅票务商品参与校验
];
if (!empty($coding)) {
$where[] = ['coding', '=', $coding];
} else {
// coding 为空时,也要防止多条空 coding + 相同日期的记录(会破坏日期切分逻辑)
$where[] = ['coding', '=', ''];
}
$conflict = Db::name('Goods')->where($where)->field('id, title, coding')->find();
if (!empty($conflict)) {
if (!empty($coding)) {
$msg = '该商品编号「' . $coding . '」在此演出日期已存在商品「' . $conflict['title'] . '」,请检查是否重复创建或修改演出日期';
} else {
$msg = '该演出日期已存在其他未设置编号的票务商品「' . $conflict['title'] . '」,请先为已有商品设置商品编号或修改演出日期';
}
return ['code' => -1, 'msg' => $msg];
}
// ──────────────────────────────────────────────────────
// 直接从数据库读 vr_goods_config全量查询不加 field 限制,避免 ThinkPHP 软删除过滤导致查不到)
if (!empty($data['vr_goods_config'])) {
$rawConfig = $data['vr_goods_config'];
} else {
$rawConfig = is_array($goodsRow) ? ($goodsRow['vr_goods_config'] ?? '') : '';
}
if (!empty($rawConfig)) {
$configs = json_decode($rawConfig, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$configs = null;
}
if (is_array($configs) && !empty($configs)) {
// 0) 重建 template_snapshot — 前端不发送 template_snapshot
// · venue总是从 DB 刷新(确保公告/须知等最新信息同步)
// · rooms仅在 snapshot 为空或有新 selected_rooms 时重建(保护用户配置)
foreach ($configs as $i => &$config) {
$templateId = intval($config['template_id'] ?? 0);
$selectedRooms = $config['selected_rooms'] ?? [];
if ($templateId > 0) {
$template = Db::name('vr_seat_templates')->find($templateId);
// 模板不存在时(硬删除场景):
// - 移除无效 config 块,避免脏数据写回
// - 前端下次打开时将看到选单为空,用户可重新选择或清空配置
if (empty($template)) {
unset($configs[$i]); // 移除无效 config 块
continue;
}
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
$allRooms = $seatMap['rooms'] ?? [];
// 总是刷新 venue 数据(包含公告/须知等最新信息),
// 确保 template_snapshot 的场馆配置与 source template 保持同步
if (empty($config['template_snapshot']) || !is_array($config['template_snapshot'])) {
$config['template_snapshot'] = [];
}
$config['template_snapshot']['venue'] = $seatMap['venue'] ?? [];
// rooms 重建条件snapshot 为空,或前端有 selected_rooms
// (避免覆盖已有 room 选择,保护用户配置)
if (!empty($selectedRooms) || empty($config['template_snapshot']['rooms'])) {
// 注意v3 格式 room.id 可能为空(用数组索引代替 id
// 此时 room_0 对应 rooms[0]room_1 对应 rooms[1],以此类推
// ── v1→v3 兼容迁移 ──
if (empty($allRooms) && !empty($seatMap['sections'])) {
$v1Sections = $seatMap['sections'] ?? [];
$v1Map = $seatMap['map'] ?? [];
$v1Seats = $seatMap['seats'] ?? [];
$v1RoomId = $selectedRooms[0] ?? 'room_1';
$allRooms = [[
'id' => $v1RoomId,
'name' => $seatMap['venue']['name'] ?? '主馆',
'sections' => $v1Sections,
'map' => $v1Map,
'seats' => $v1Seats,
]];
}
// 按 selected_rooms 过滤(支持前端标准化的 "room_0" 格式双向兼容)
// 注意v3 格式 room.id 可能为空(用数组索引代替 id
// 此时 room_0 对应 rooms[0]room_1 对应 rooms[1],以此类推
$selectedRoomIds = array_column(
array_filter($allRooms, function ($r) use ($selectedRooms) {
$rid = $r['id'] ?? '';
// 直接匹配
if (in_array($rid, $selectedRooms)) {
return true;
}
// 尝试加/减 "room_" 前缀匹配PHP 7.x 兼容)
if (strpos($rid, 'room_') === 0 && in_array(substr($rid, 5), $selectedRooms)) {
return true;
}
if (!empty($rid) && in_array('room_' . $rid, $selectedRooms)) {
return true;
}
// 空 id用数组索引替代room_0→rooms[0], room_1→rooms[1]
static $roomIndex = -1;
$roomIndex++;
if ($rid === '' && in_array('room_' . $roomIndex, $selectedRooms)) {
return true;
}
return false;
}), null
);
$config['template_snapshot']['rooms'] = $selectedRoomIds;
}
}
}
unset($config); // 解除引用,避免后续误改
$configs = array_values($configs); // 重排数组索引
// 将填充后的完整 config 写回 goods 表(仅在有有效配置时)
if (!empty($configs)) {
Db::name('Goods')->where('id', $goodsId)->update([
'vr_goods_config' => json_encode($configs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
}
// a) 清空原生规格数据 —— 避免列偏移
Db::name('GoodsSpecType')->where('goods_id', $goodsId)->delete();
Db::name('GoodsSpecBase')->where('goods_id', $goodsId)->delete();
Db::name('GoodsSpecValue')->where('goods_id', $goodsId)->delete();
// b) 逐模板生成 VR SKUensureAndFillVrSpecTypes 在内部调用type.value 同步写入)
foreach ($configs as $config) {
$templateId = intval($config['template_id'] ?? 0);
$selectedRooms = $config['selected_rooms'] ?? [];
$selectedSections = $config['selected_sections'] ?? [];
$sessions = $config['sessions'] ?? [];
if ($templateId > 0) {
$res = SeatSkuService::BatchGenerate(
$goodsId, $templateId,
$selectedRooms, $selectedSections, $sessions
);
if ($res['code'] !== 0) {
return $res;
}
}
}
// c) 重新计算 goods.price / goods.inventory
SeatSkuService::refreshGoodsBase($goodsId);
}
}
}
return ['code' => 0];
}
return ['code' => 0];
}
}