diff --git a/.gitignore b/.gitignore index 452c557..fede1f1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ shopxo/public/adminufgeyw.php .claude/ .gitnexus/ graphify-out/ + +# Local test files +docs/test.json +shopxo/public/testh5/ diff --git a/docs/bug_analysis_checkout_geo_filter.md b/docs/bug_analysis_checkout_geo_filter.md new file mode 100644 index 0000000..d3f08ef --- /dev/null +++ b/docs/bug_analysis_checkout_geo_filter.md @@ -0,0 +1,50 @@ +# Bug Analysis: Checkout Failed with "Data does not exist or has been deleted" due to Geolocation Filtering + +## Background +The `vr_ticket` plugin implements a geolocation-based filtering mechanism that automatically filters events and ticket products based on the user's physical GPS coordinates. This ensures that users see local events by default on lists and discovery feeds. + +## Issue Description +When a user attempts to place an order or view the checkout screen, the API request (`api.php?s=buy/index`) contains the user's current GPS location coordinates (`user_lng` and `user_lat`) sent automatically by the client application (uni-app/H5). + +The order confirmation process queries the database via `BuyService::BuyGoods()` -> `GoodsService::GoodsList()`. The geolocation hook `OnGoodsListBegin()` intercepts this database query, resolves the user's current city based on their coordinates, and restricts the query to that city. + +If a user located in city A (e.g. Xiamen) attempts to buy a ticket for an event in city B (e.g. Beijing), the coordinate-based city filter restricts the query to city A. Since the product's `produce_region` is set to city B, the database query returns no results. This triggers the following error response in the checkout API: +```json +{"msg":"数据不存在或已删除","code":-10,"data":""} +``` + +--- + +## Root Cause +The `plugins_service_goods_list_begin` hook is invoked inside `GoodsService::GoodsList()`. This hook did not distinguish between **discovery/browsing contexts** (where filtering is desired) and **checkout/detail/purchase contexts** (where the user has already chosen a product and must be allowed to view/buy it regardless of their current physical coordinates). + +--- + +## Resolution +We added a bypass check at the very beginning of the `OnGoodsListBegin` hook in [Hook.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/Hook.php#L547-L552): + +```php +// 订单确认/下单/购物车/商品详情/订单等流程跳过坐标城市筛选,避免因 GPS 坐标导致商品被过滤 +$controller = strtolower(request()->controller()); +if (isset($paramsArr['buy_type']) || in_array($controller, ['buy', 'goods', 'cart', 'order', 'orderaftersale'])) { + return; +} +``` + +### Bypass Conditions +The city filter is now skipped under any of the following conditions: +1. `buy_type` request parameter is present (indicates checkout/buying process). +2. The current controller is: + - `buy`: Checkout/Order placement page. + - `goods`: Product detail page. + - `cart`: Shopping cart view. + - `order` / `orderaftersale`: User orders and after-sale management pages. + +This ensures that coordinates-based filtering is strictly confined to lists, search pages, and category pages, while checkout and detail queries are bypassed. + +--- + +## Verification & Status +- **File modified**: [shopxo/app/plugins/vr_ticket/Hook.php](file:///Users/bigemon/WorkSpace/vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/Hook.php) +- **Impact Risk**: **LOW** (verified via GitNexus impact analysis tool). +- **Result**: Confirmed that queries within checkout and product details pages will successfully bypass the geolocation filter and proceed to load the selected ticket product information, successfully resolving the `-10` error. diff --git a/shopxo/app/event.php b/shopxo/app/event.php index d81562d..5e6b5d9 100644 --- a/shopxo/app/event.php +++ b/shopxo/app/event.php @@ -81,6 +81,10 @@ return array ( array ( 0 => 'app\\plugins\\vr_ticket\\Hook', ), + 'plugins_service_buy_order_insert_begin' => + array ( + 0 => 'app\\plugins\\vr_ticket\\Hook', + ), 'plugins_view_admin_goods_save' => array ( 0 => 'app\\plugins\\vr_ticket\\hook\\AdminGoodsSave', diff --git a/shopxo/app/plugins/vr_ticket/Hook.php b/shopxo/app/plugins/vr_ticket/Hook.php index 6548ea5..f66b8a8 100644 --- a/shopxo/app/plugins/vr_ticket/Hook.php +++ b/shopxo/app/plugins/vr_ticket/Hook.php @@ -387,9 +387,14 @@ class Hook // Step 3: 观影人信息校验(新增) // ────────────────────────────────────────────────────── // 1. 获取前端提交的 viewer_data - $viewerData = $params['data']['viewer_data'] ?? []; + $viewerData = $params['params']['viewer_data'] ?? request()->param('viewer_data') ?? []; if (!is_array($viewerData)) { - $viewerData = []; + if (is_string($viewerData)) { + $viewerData = json_decode($viewerData, true); + } + if (!is_array($viewerData)) { + $viewerData = []; + } } // 2. 获取商品配置中的观影人要求 @@ -484,6 +489,25 @@ class Hook } } + // 5. 校验通过,写入 order 数据库需要读取的 extension_data.attendee + if (!empty($viewerData)) { + $viewer = $viewerData[0]; + $attendee = [ + 'real_name' => trim($viewer['name'] ?? ''), + 'phone' => trim($viewer['mobile'] ?? ''), + 'id_card' => trim($viewer['idcard'] ?? ''), + ]; + + if (isset($params['order'])) { + $ext = json_decode($params['order']['extension_data'] ?? '{}', true); + if (!is_array($ext)) { + $ext = []; + } + $ext['attendee'] = $attendee; + $params['order']['extension_data'] = json_encode($ext, JSON_UNESCAPED_UNICODE); + } + } + return ['code' => 0, 'msg' => '']; } @@ -520,6 +544,12 @@ class Hook $paramsArr = $params['params'] ?? []; + // 订单确认/下单/购物车/商品详情/订单等流程跳过坐标城市筛选,避免因 GPS 坐标导致商品被过滤 + $controller = strtolower(request()->controller()); + if (isset($paramsArr['buy_type']) || in_array($controller, ['buy', 'goods', 'cart', 'order', 'orderaftersale'])) { + return; + } + // 优先级1:显式传入的 city_id / cityid $cityId = isset($paramsArr['city_id']) ? intval($paramsArr['city_id']) : 0; if ($cityId <= 0) { @@ -551,9 +581,9 @@ class Hook // 使用闭包实现 OR 条件:vr_goods_config 为空(非票务) OR produce_region = 城市ID $where[] = function($query) use ($cityId) { $query->where(function($q) { - $q->where('g.vr_goods_config', '=', ''); + $q->where('vr_goods_config', '=', '')->whereOr('vr_goods_config', 'NULL'); })->whereOr(function($q) use ($cityId) { - $q->where('g.vr_goods_config', '<>', '')->where('g.produce_region', '=', $cityId); + $q->where('vr_goods_config', '<>', '')->where('produce_region', '=', $cityId); }); }; // @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnGoodsListBegin: city filter applied to ticket goods only, cityId=$cityId\n", FILE_APPEND); @@ -576,7 +606,7 @@ class Hook $paramsArr = $params['params'] ?? []; - // 优先级1:显式传入的 city_id / cityid + // 优先级1:显式传入 the city_id / cityid $cityId = isset($paramsArr['city_id']) ? intval($paramsArr['city_id']) : 0; if ($cityId <= 0) { $cityId = isset($paramsArr['cityid']) ? intval($paramsArr['cityid']) : 0; @@ -609,9 +639,9 @@ class Hook // 使用闭包实现 OR 条件:vr_goods_config 为空(非票务) OR produce_region = 城市ID $whereBase[] = function($query) use ($cityId) { $query->where(function($q) { - $q->where('g.vr_goods_config', '=', ''); + $q->where('vr_goods_config', '=', '')->whereOr('vr_goods_config', 'NULL'); })->whereOr(function($q) use ($cityId) { - $q->where('g.vr_goods_config', '<>', '')->where('g.produce_region', '=', $cityId); + $q->where('vr_goods_config', '<>', '')->where('produce_region', '=', $cityId); }); }; // [调试代码] @file_put_contents($debugFile, date('Y-m-d H:i:s') . " OnSearchListBegin: city filter applied to ticket goods only, cityId=$cityId\n", FILE_APPEND); diff --git a/shopxo/app/plugins/vr_ticket/config.json b/shopxo/app/plugins/vr_ticket/config.json index a43fc66..b611c83 100644 --- a/shopxo/app/plugins/vr_ticket/config.json +++ b/shopxo/app/plugins/vr_ticket/config.json @@ -35,6 +35,9 @@ "plugins_service_order_delete_success": [ "app\\plugins\\vr_ticket\\Hook" ], + "plugins_service_buy_order_insert_begin": [ + "app\\plugins\\vr_ticket\\Hook" + ], "plugins_view_admin_goods_save": [ "app\\plugins\\vr_ticket\\hook\\AdminGoodsSave" ],