Compare commits
No commits in common. "main" and "fix/ticket-select-refresh" have entirely different histories.
main
...
fix/ticket
|
|
@ -22,7 +22,3 @@ shopxo/public/adminufgeyw.php
|
|||
.claude/
|
||||
.gitnexus/
|
||||
graphify-out/
|
||||
|
||||
# Local test files
|
||||
docs/test.json
|
||||
shopxo/public/testh5/
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -81,10 +81,6 @@ 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',
|
||||
|
|
|
|||
|
|
@ -387,15 +387,10 @@ class Hook
|
|||
// Step 3: 观影人信息校验(新增)
|
||||
// ──────────────────────────────────────────────────────
|
||||
// 1. 获取前端提交的 viewer_data
|
||||
$viewerData = $params['params']['viewer_data'] ?? request()->param('viewer_data') ?? [];
|
||||
if (!is_array($viewerData)) {
|
||||
if (is_string($viewerData)) {
|
||||
$viewerData = json_decode($viewerData, true);
|
||||
}
|
||||
$viewerData = $params['data']['viewer_data'] ?? [];
|
||||
if (!is_array($viewerData)) {
|
||||
$viewerData = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取商品配置中的观影人要求
|
||||
$viewerConfig = null;
|
||||
|
|
@ -489,25 +484,6 @@ 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' => ''];
|
||||
}
|
||||
|
||||
|
|
@ -544,12 +520,6 @@ 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) {
|
||||
|
|
@ -581,9 +551,9 @@ class Hook
|
|||
// 使用闭包实现 OR 条件:vr_goods_config 为空(非票务) OR produce_region = 城市ID
|
||||
$where[] = function($query) use ($cityId) {
|
||||
$query->where(function($q) {
|
||||
$q->where('vr_goods_config', '=', '')->whereOr('vr_goods_config', 'NULL');
|
||||
$q->where('g.vr_goods_config', '=', '');
|
||||
})->whereOr(function($q) use ($cityId) {
|
||||
$q->where('vr_goods_config', '<>', '')->where('produce_region', '=', $cityId);
|
||||
$q->where('g.vr_goods_config', '<>', '')->where('g.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);
|
||||
|
|
@ -606,7 +576,7 @@ class Hook
|
|||
|
||||
$paramsArr = $params['params'] ?? [];
|
||||
|
||||
// 优先级1:显式传入 the city_id / cityid
|
||||
// 优先级1:显式传入的 city_id / cityid
|
||||
$cityId = isset($paramsArr['city_id']) ? intval($paramsArr['city_id']) : 0;
|
||||
if ($cityId <= 0) {
|
||||
$cityId = isset($paramsArr['cityid']) ? intval($paramsArr['cityid']) : 0;
|
||||
|
|
@ -639,9 +609,9 @@ class Hook
|
|||
// 使用闭包实现 OR 条件:vr_goods_config 为空(非票务) OR produce_region = 城市ID
|
||||
$whereBase[] = function($query) use ($cityId) {
|
||||
$query->where(function($q) {
|
||||
$q->where('vr_goods_config', '=', '')->whereOr('vr_goods_config', 'NULL');
|
||||
$q->where('g.vr_goods_config', '=', '');
|
||||
})->whereOr(function($q) use ($cityId) {
|
||||
$q->where('vr_goods_config', '<>', '')->where('produce_region', '=', $cityId);
|
||||
$q->where('g.vr_goods_config', '<>', '')->where('g.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);
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* VR票务插件 - 坐标反查城市 API 控制器
|
||||
*
|
||||
* 路由:/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=geo&pluginsaction=city
|
||||
* → class = \app\plugins\vr_ticket\api\Geo (ucfirst('geo') = 'Geo')
|
||||
* → method = ucfirst('city') = 'City'
|
||||
* → \app\plugins\vr_ticket\api\Geo::City($params)
|
||||
*
|
||||
* 用途:根据经纬度反查最近的可服务城市(基于 Region 表,离线查找)
|
||||
* 不依赖:任何地图厂商 AK / 外部 SDK
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 仅返回可服务城市(Region 表里有坐标 + is_enable=1 的城市)
|
||||
* 2. 距离超过 150km 视为"无附近服务城市"
|
||||
* 3. 缓存按 5 位小数(~1m)聚合,30 天有效
|
||||
* 4. 失败时不抛异常,统一返回 DataReturn 格式
|
||||
*
|
||||
* @package vr_ticket\api
|
||||
*/
|
||||
|
||||
namespace app\plugins\vr_ticket\api;
|
||||
|
||||
use app\plugins\vr_ticket\service\GeoCityService;
|
||||
|
||||
class Geo
|
||||
{
|
||||
/**
|
||||
* 无附近城市的最大距离阈值(km)
|
||||
* Region 表只存"我们提供服务的城市"坐标;超出此值视为超出服务范围
|
||||
*/
|
||||
const MAX_NEARBY_DISTANCE_KM = 150;
|
||||
|
||||
/**
|
||||
* 缓存天数
|
||||
*/
|
||||
const CACHE_DAYS = 30;
|
||||
|
||||
/**
|
||||
* 根据坐标获取最近的可服务城市
|
||||
*
|
||||
* @param array $params 必须包含 lng, lat
|
||||
* @return array DataReturn 结构 { code, msg, data }
|
||||
*/
|
||||
public function City($params = [])
|
||||
{
|
||||
// 1. 参数校验
|
||||
$lng = isset($params['lng']) ? $params['lng'] : '';
|
||||
$lat = isset($params['lat']) ? $params['lat'] : '';
|
||||
if ($lng === '' || $lat === '') {
|
||||
return DataReturn('lng/lat 必填', -1);
|
||||
}
|
||||
if (!is_numeric($lng) || !is_numeric($lat)) {
|
||||
return DataReturn('lng/lat 必须为数字', -1);
|
||||
}
|
||||
$lng = (float)$lng;
|
||||
$lat = (float)$lat;
|
||||
if ($lng < -180 || $lng > 180 || $lat < -90 || $lat > 90) {
|
||||
return DataReturn('坐标越界', -1);
|
||||
}
|
||||
|
||||
// 2. 缓存命中优先(按 ~1m 精度聚合,30 天)
|
||||
$cache_key = 'vr_ticket_geo_regeocode_' . round($lng, 5) . '_' . round($lat, 5);
|
||||
$cached = MyCache($cache_key);
|
||||
if (is_array($cached) && isset($cached['city'])) {
|
||||
return DataReturn('success', 0, $cached);
|
||||
}
|
||||
|
||||
// 3. 查 Region 表
|
||||
// FindNearestCity 已在 2026-06-28 修复距离单位 bug
|
||||
// 修复前 distance = 真实 km 数 / 1000(错误)
|
||||
// 修复后 distance = 真实 km 数(正确)
|
||||
$nearest = GeoCityService::FindNearestCity($lng, $lat, 3);
|
||||
|
||||
// 4. 没匹配到任何城市
|
||||
if (empty($nearest) || empty($nearest['id']) || empty($nearest['name'])) {
|
||||
return DataReturn('暂无服务城市数据', -1);
|
||||
}
|
||||
|
||||
// 5. 距离超阈值 → 视为无附近服务城市
|
||||
$distance_km = (float)($nearest['distance'] ?? 0);
|
||||
if ($distance_km > self::MAX_NEARBY_DISTANCE_KM) {
|
||||
return DataReturn(
|
||||
'当前位置暂无服务城市(最近 ' . $nearest['name'] . ' 约 ' . $distance_km . 'km)',
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
// 6. 组装统一返回
|
||||
$result = [
|
||||
'city' => (string)$nearest['name'],
|
||||
'city_id' => (int)$nearest['id'],
|
||||
'distance' => $distance_km,
|
||||
'input' => [
|
||||
'lng' => $lng,
|
||||
'lat' => $lat,
|
||||
],
|
||||
'matched' => [
|
||||
'lng' => (float)($nearest['lng'] ?? 0),
|
||||
'lat' => (float)($nearest['lat'] ?? 0),
|
||||
],
|
||||
];
|
||||
|
||||
// 7. 写缓存
|
||||
MyCache($cache_key, $result, 86400 * self::CACHE_DAYS);
|
||||
|
||||
return DataReturn('success', 0, $result);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,9 +35,6 @@
|
|||
"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"
|
||||
],
|
||||
|
|
@ -47,9 +44,6 @@
|
|||
"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,50 +360,6 @@ 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)
|
||||
|
|
@ -527,8 +483,6 @@ class AdminGoodsSave
|
|||
|
||||
watch(isTicket, (val) => {
|
||||
applyTicketRequired(val);
|
||||
// 票务商品时锁定 site_type 为"虚拟";取消时解锁
|
||||
syncSiteTypeLock(val);
|
||||
if (val) {
|
||||
// 票务商品模式:替换城市下拉选项
|
||||
replaceCityOptions();
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
<?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,12 +26,6 @@ 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);
|
||||
|
|
|
|||
|
|
@ -116,8 +116,7 @@ class BaseService
|
|||
if (empty($goods)) {
|
||||
return false;
|
||||
}
|
||||
// 严格:只检查 item_type 字段,避免因 goods 表无 venue_data 列导致所有商品都通过检查的问题
|
||||
return ($goods['item_type'] ?? '') === 'ticket';
|
||||
return !empty($goods['venue_data']) || ($goods['item_type'] ?? '') === 'ticket';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class GeoCityService
|
|||
$region['lat'],
|
||||
$lng,
|
||||
$lat,
|
||||
2 // 返回 km(GeoTransUtil::GetDistance 第 4 参 2=km)
|
||||
2 // 返回米
|
||||
);
|
||||
|
||||
if ($distance < $min_distance) {
|
||||
|
|
@ -123,7 +123,7 @@ class GeoCityService
|
|||
$nearest = [
|
||||
'id' => $region['id'],
|
||||
'name' => $region['name'],
|
||||
'distance' => round($distance, 2), // km(GetDistance 第 4 参 2 已是 km,保留 2 位小数)
|
||||
'distance' => round($distance / 1000, 2), // 转为km,保留2位小数
|
||||
'lng' => $region['lng'],
|
||||
'lat' => $region['lat'],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -112,10 +112,6 @@ class TicketService extends BaseService
|
|||
// 逐个生成票(每个订单明细行 = 一张票)
|
||||
$count = 0;
|
||||
foreach ($order_goods as $og) {
|
||||
// 过滤非票务商品(如果被上面的循环 continue 跳过了,就不会有 _parsed_spec_name)
|
||||
if (!isset($og['_parsed_spec_name'])) {
|
||||
continue;
|
||||
}
|
||||
$ticket_id = self::issueTicket($order, $og);
|
||||
if ($ticket_id > 0) {
|
||||
$count++;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ $store_http = (MyFileConfig('common_is_https_connect_store') == 1) ? 'https://'
|
|||
// 配置信息
|
||||
return [
|
||||
// 开发模式
|
||||
'is_develop' => false,
|
||||
'is_develop' => true,
|
||||
|
||||
// 默认编码
|
||||
'default_charset' => 'utf-8',
|
||||
|
|
|
|||
|
|
@ -1,263 +0,0 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ShopXO 国内领先企业级B2C免费开源电商系统
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2011~2019 http://shopxo.net All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Devil
|
||||
// +----------------------------------------------------------------------
|
||||
namespace payment;
|
||||
|
||||
use think\facade\Db;
|
||||
use app\service\PaymentService;
|
||||
use app\service\OrderService;
|
||||
use app\plugins\wallet\service\WalletService;
|
||||
use app\plugins\scanpay\service\ScanpayLogService;
|
||||
use app\plugins\membershiplevelvip\service\PayService as LevelPayService;
|
||||
|
||||
/**
|
||||
* 钱包支付
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @date 2019-06-16
|
||||
* @desc description
|
||||
*/
|
||||
class WalletPay
|
||||
{
|
||||
// 插件配置参数
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @date 2018-09-17
|
||||
* @desc description
|
||||
* @param [array] $params [输入参数(支付配置参数)]
|
||||
*/
|
||||
public function __construct($params = [])
|
||||
{
|
||||
$this->config = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @date 2019-06-16
|
||||
* @desc description
|
||||
*/
|
||||
public function Config()
|
||||
{
|
||||
// 基础信息
|
||||
$base = [
|
||||
'name' => '钱包支付', // 插件名称
|
||||
'version' => '0.0.5', // 插件版本
|
||||
'apply_version' => '不限', // 适用系统版本描述
|
||||
'desc' => '钱包余额支付', // 插件描述(支持html)
|
||||
'author' => 'Devil', // 开发者
|
||||
'author_url' => 'http://shopxo.net/', // 开发者主页
|
||||
];
|
||||
|
||||
// 配置信息
|
||||
$element = [];
|
||||
|
||||
return [
|
||||
'base' => $base,
|
||||
'element' => $element,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 钱包校验
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @datetime 2019-06-16T17:17:58+0800
|
||||
* @param [array] $params [输入参数]
|
||||
*/
|
||||
private function Check($params = [])
|
||||
{
|
||||
// 钱包校验
|
||||
$wallet = Db::name('Plugins')->where(['plugins'=>'wallet'])->find();
|
||||
if(empty($wallet))
|
||||
{
|
||||
return DataReturn('请先安装钱包插件[ Wallet ]', -1);
|
||||
}
|
||||
return DataReturn('钱包正常', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付入口
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @date 2019-06-16
|
||||
* @desc description
|
||||
* @param [array] $params [输入参数]
|
||||
*/
|
||||
public function Pay($params = [])
|
||||
{
|
||||
// 校验
|
||||
$ret = $this->Check($params);
|
||||
if($ret['code'] != 0)
|
||||
{
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// 是否登录用户
|
||||
if(empty($params['user']) || empty($params['user']['id']))
|
||||
{
|
||||
return DataReturn('请先登录后再使用钱包支付!', -1);
|
||||
}
|
||||
|
||||
// 获取用户钱包校验
|
||||
$user_wallet = WalletService::UserWallet($params['user']['id']);
|
||||
if($user_wallet['code'] != 0)
|
||||
{
|
||||
return $user_wallet;
|
||||
}
|
||||
|
||||
// 余额校验
|
||||
if($user_wallet['data']['normal_money'] < $params['total_price'])
|
||||
{
|
||||
return DataReturn('钱包余额不足['.$user_wallet['data']['normal_money'].'元]', -10);
|
||||
}
|
||||
|
||||
// 处理支付
|
||||
$ret = WalletService::UserWalletMoneyUpdate($params['user']['id'], $params['total_price'], 0, 'normal_money', 3, $params['name'].'[订单'.$params['order_no'].']', ['is_consistent'=>1]);
|
||||
if($ret['code'] == 0)
|
||||
{
|
||||
// 支付方式
|
||||
$payment = PaymentService::PaymentList(['where'=>['payment'=>'WalletPay']]);
|
||||
|
||||
// 获取订单日志信息
|
||||
$pay_log_data = Db::name('PayLog')->find($params['order_id']);
|
||||
if(empty($pay_log_data))
|
||||
{
|
||||
return DataReturn('日志订单有误', -1);
|
||||
}
|
||||
|
||||
// 获取关联信息
|
||||
$pay_log_value = Db::name('PayLogValue')->where(['pay_log_id'=>$pay_log_data['id']])->column('business_id');
|
||||
if(empty($pay_log_value))
|
||||
{
|
||||
return DataReturn('日志订单关联信息有误', -1);
|
||||
}
|
||||
|
||||
// 根据业务类型
|
||||
$business_type = empty($params['business_type']) ? 'system-order' : $params['business_type'];
|
||||
|
||||
// 获取对应订单信息
|
||||
$order_list = [];
|
||||
switch($business_type)
|
||||
{
|
||||
// 系统订单
|
||||
case 'system-order' :
|
||||
$order_list = Db::name('Order')->where(['id'=>$pay_log_value, 'status'=>1])->select()->toArray();
|
||||
break;
|
||||
|
||||
// 扫码收款
|
||||
case 'plugins-scanpay' :
|
||||
$order_list = Db::name('PluginsScanpayLog')->where(['id'=>$pay_log_value, 'status'=>0])->select()->toArray();
|
||||
break;
|
||||
|
||||
// 会员等级
|
||||
case 'plugins-membershiplevelvip' :
|
||||
$order_list = Db::name('PluginsMembershiplevelvipPaymentUserOrder')->where(['id'=>$pay_log_value, 'status'=>0])->select()->toArray();
|
||||
break;
|
||||
}
|
||||
if(empty($order_list))
|
||||
{
|
||||
return DataReturn('订单信息有误', -1);
|
||||
}
|
||||
|
||||
// 订单数量是否一致
|
||||
if(count($order_list) != count($pay_log_value))
|
||||
{
|
||||
return DataReturn('订单与日志记录数量不一致', -1);
|
||||
}
|
||||
|
||||
// 支付处理
|
||||
$pay_params = [
|
||||
'order' => $order_list,
|
||||
'payment' => $payment[0],
|
||||
'pay_log_data' => $pay_log_data,
|
||||
'pay' => [
|
||||
'trade_no' => 'wallet',
|
||||
'subject' => $pay_log_data['subject'],
|
||||
'buyer_user' => (empty($params['user']) || empty($params['user']['user_name_view'])) ? '' : $params['user']['user_name_view'],
|
||||
'pay_price' => $pay_log_data['total_price'],
|
||||
],
|
||||
];
|
||||
|
||||
// 调用支付处理方法
|
||||
switch($business_type)
|
||||
{
|
||||
// 系统订单
|
||||
case 'system-order' :
|
||||
$ret = OrderService::OrderPayHandle($pay_params);
|
||||
if($ret['code'] == 0)
|
||||
{
|
||||
return DataReturn('支付成功', 0, MyUrl('index/order/respond', ['appoint_status'=>0]));
|
||||
}
|
||||
break;
|
||||
|
||||
// 扫码收款
|
||||
case 'plugins-scanpay' :
|
||||
$pay_params['order'] = $pay_params['order'][0];
|
||||
$ret = ScanpayLogService::ScanpayLogHandle($pay_params);
|
||||
if($ret['code'] == 0)
|
||||
{
|
||||
return DataReturn('支付成功', 0, PluginsHomeUrl('scanpay', 'index', 'respond'));
|
||||
}
|
||||
break;
|
||||
|
||||
// 会员购买
|
||||
case 'plugins-membershiplevelvip' :
|
||||
$pay_params['order'] = $pay_params['order'][0];
|
||||
$ret = LevelPayService::LevelPayHandle($pay_params);
|
||||
if($ret['code'] == 0)
|
||||
{
|
||||
return DataReturn('支付成功', 0, PluginsHomeUrl('membershiplevelvip', 'buy', 'respond', ['appoint_status'=>0]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付回调处理
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @date 2019-06-16
|
||||
* @desc description
|
||||
* @param [array] $params [输入参数]
|
||||
*/
|
||||
public function Respond($params = [])
|
||||
{
|
||||
return DataReturn('处理成功', 0, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款处理
|
||||
* @author Devil
|
||||
* @blog http://gong.gg/
|
||||
* @version 1.0.0
|
||||
* @date 2019-06-16
|
||||
* @desc description
|
||||
* @param [array] $params [输入参数]
|
||||
*/
|
||||
public function Refund($params = [])
|
||||
{
|
||||
return DataReturn('请选择退至钱包', -1);
|
||||
}
|
||||
}
|
||||
?>
|
||||
Loading…
Reference in New Issue