比较提交
8 次代码提交
9607e2c251
...
main
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
1e33c14bb4 | ||
|
|
f93f88fa26 | ||
|
|
7ea240dae4 | ||
|
|
1fc5138362 | ||
|
|
728dd8b8f1 | ||
|
|
afac67a101 | ||
|
|
c4d7c1fce8 | ||
|
|
2456ad6945 |
110
addons/shopro/controller/Address.php
普通文件
110
addons/shopro/controller/Address.php
普通文件
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\controller;
|
||||
|
||||
use addons\shopro\exception\Exception;
|
||||
use addons\shopro\model\UserAddress;
|
||||
use addons\shopro\model\Area;
|
||||
|
||||
class Address extends Base
|
||||
{
|
||||
|
||||
protected $noNeedLogin = ['area'];
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->success('收货地址', UserAddress::getUserAddress());
|
||||
}
|
||||
|
||||
public function defaults()
|
||||
{
|
||||
$this->success('默认收货地址', UserAddress::getUserDefaultAddress());
|
||||
}
|
||||
|
||||
public function area()
|
||||
{
|
||||
$cacheKey = 'shopro_area_tree_data';
|
||||
$data = cache($cacheKey);
|
||||
if (!$data) {
|
||||
// 一次性查出全部省市区数据(仅 1 次查询),并在内存中按 pid 构建树形,消除 350+ 次嵌套 SQL 击穿
|
||||
$allAreas = Area::where('level', 'in', [1, 2, 3])
|
||||
->field('id as value, name as label, pid, level')
|
||||
->order('id asc')
|
||||
->select();
|
||||
|
||||
$provinceData = [];
|
||||
$cityMap = [];
|
||||
$areaMap = [];
|
||||
foreach ($allAreas as $item) {
|
||||
if ($item['level'] == 1) {
|
||||
$provinceData[] = $item;
|
||||
} elseif ($item['level'] == 2) {
|
||||
$cityMap[$item['pid']][] = $item;
|
||||
} elseif ($item['level'] == 3) {
|
||||
$areaMap[$item['pid']][] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$cityData = [];
|
||||
$areaData = [];
|
||||
foreach ($provinceData as $k => $p) {
|
||||
$cities = $cityMap[$p['value']] ?? [];
|
||||
$cityData[$k] = $cities;
|
||||
foreach ($cities as $i => $c) {
|
||||
$areaData[$k][$i] = $areaMap[$c['value']] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'provinceData' => $provinceData,
|
||||
'cityData' => $cityData,
|
||||
'areaData' => $areaData
|
||||
];
|
||||
cache($cacheKey, $data, 604800);
|
||||
}
|
||||
|
||||
$this->success('省市区', $data);
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
|
||||
// 表单验证
|
||||
$this->shoproValidate($params, get_class(), 'edit');
|
||||
|
||||
$this->success('编辑地址', UserAddress::edit($params));
|
||||
}
|
||||
|
||||
public function info()
|
||||
{
|
||||
$params = $this->request->get();
|
||||
$this->success('地址详情', UserAddress::info($params));
|
||||
}
|
||||
|
||||
public function del()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
$this->success('地址详情', UserAddress::del($params));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function regeo()
|
||||
{
|
||||
$latitude = $this->request->post('latitude');
|
||||
$longitude = $this->request->post('longitude');
|
||||
if (!is_numeric($latitude) || !is_numeric($longitude) || abs($latitude) < 0.000001 || abs($longitude) < 0.000001) {
|
||||
$this->error('请先选择位置');
|
||||
}
|
||||
try {
|
||||
$data = \app\common\library\Tianditu::reverse((float)$longitude, (float)$latitude);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage() ?: '地址解析失败');
|
||||
}
|
||||
$this->success('解析成功', $data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -107,6 +107,9 @@ class Pay extends Base
|
||||
if (strpos($order_sn, 'TO') !== 0) {
|
||||
// 保持 order_sn 不变,避免支付回调时找不到订单
|
||||
if ($payment == 'group') {
|
||||
if (!$user) {
|
||||
$this->error('请先登录', null, 401);
|
||||
}
|
||||
$payment = 'wechat';
|
||||
// 重新计算价格
|
||||
$order_data['total_fee'] = bcsub($order_data['total_fee'], $user->money, 2);
|
||||
@@ -342,12 +345,44 @@ class Pay extends Base
|
||||
// 支付成功流程
|
||||
$pay_fee = $payment == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
|
||||
|
||||
// 你可以在此编写订单逻辑
|
||||
$order = $order->nopay()->where('order_sn', $out_trade_no)->lock(true)->find();
|
||||
$timeStr = $data['time_end'] ?? ($data['gmt_payment'] ?? '');
|
||||
$notify_time = $timeStr && ($ts = strtotime($timeStr)) ? date('Y-m-d H:i:s', $ts) : date('Y-m-d H:i:s');
|
||||
|
||||
if (!$order) {
|
||||
// 已支付则直接返回成功,否则记录日志并通知微信重试
|
||||
$paidOrder = Order::where('order_sn', $out_trade_no)->where('status', '>', 0)->find();
|
||||
$payment_json = json_encode(
|
||||
$data instanceof \think\Collection || (is_object($data) && method_exists($data, 'all'))
|
||||
? $data->all()
|
||||
: (array)$data
|
||||
);
|
||||
|
||||
$buyer_email = $payment == 'alipay' ? ($data['buyer_logon_id'] ?? ($data['buyer_id'] ?? '')) : ($data['openid'] ?? '');
|
||||
$transaction_id = $payment == 'alipay' ? ($data['trade_no'] ?? '') : ($data['transaction_id'] ?? '');
|
||||
|
||||
$isProcessed = false;
|
||||
Db::transaction(function () use ($order, $out_trade_no, $transaction_id, $notify_time, $buyer_email, $payment_json, $pay_fee, $payment, &$isProcessed) {
|
||||
// 必须在事务内加排他锁复查,确保高并发回调互斥幂等
|
||||
$lockOrder = $order->nopay()->where('order_sn', $out_trade_no)->lock(true)->find();
|
||||
if (!$lockOrder) {
|
||||
$isProcessed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$notify = [
|
||||
'order_sn' => $out_trade_no,
|
||||
'transaction_id' => $transaction_id,
|
||||
'notify_time' => $notify_time,
|
||||
'buyer_email' => $buyer_email,
|
||||
'payment_json' => $payment_json,
|
||||
'pay_fee' => $pay_fee,
|
||||
'pay_type' => $lockOrder->pay_type == 'group' ? 'group' : $payment
|
||||
];
|
||||
|
||||
$lockOrder->paymentProcess($lockOrder, $notify);
|
||||
});
|
||||
|
||||
if ($isProcessed) {
|
||||
// 已经被并发处理过,直接向网关确认成功(使用全新实例避免前置查询条件残留)
|
||||
list($paidCheckOrder) = $this->getOrderInstance($out_trade_no);
|
||||
$paidOrder = $paidCheckOrder->where('order_sn', $out_trade_no)->where('status', '>', 0)->find();
|
||||
if ($paidOrder) {
|
||||
return $pay->success()->send();
|
||||
}
|
||||
@@ -355,20 +390,6 @@ class Pay extends Base
|
||||
return false;
|
||||
}
|
||||
|
||||
Db::transaction(function () use ($order, $data, $payment, $platform, $pay_fee, $prepay_type) {
|
||||
$notify = [
|
||||
'order_sn' => $data['out_trade_no'],
|
||||
'transaction_id' => $payment == 'alipay' ? $data['trade_no'] : $data['transaction_id'],
|
||||
'notify_time' => date('Y-m-d H:i:s', strtotime($data['time_end'])),
|
||||
'buyer_email' => $payment == 'alipay' ? $data['buyer_logon_id'] : $data['openid'],
|
||||
'payment_json' => json_encode($data->all()),
|
||||
'pay_fee' => $pay_fee,
|
||||
'pay_type' => $order->pay_type == 'group' ? 'group' : $payment // 支付方式
|
||||
];
|
||||
|
||||
$order->paymentProcess($order, $notify);
|
||||
});
|
||||
|
||||
return $pay->success()->send();
|
||||
} catch (\Exception $e) {
|
||||
Log::write('notifyx-error:' . $e->getMessage());
|
||||
|
||||
@@ -236,6 +236,20 @@ class PayService
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 主动查询网关侧订单状态(支付对账/掉单补偿用,无需验签回调)
|
||||
public function query($out_trade_no)
|
||||
{
|
||||
$pay = $this->getPay();
|
||||
|
||||
$order = ['out_trade_no' => $out_trade_no];
|
||||
|
||||
if ($this->payment === 'wechat') {
|
||||
return $pay->find($order);
|
||||
}
|
||||
|
||||
return $pay->find($order, 'web');
|
||||
}
|
||||
|
||||
|
||||
public function notifyRefund($callback)
|
||||
{
|
||||
|
||||
@@ -800,9 +800,12 @@ trait OrderOper
|
||||
$user = User::where('id', $order->user_id)->find();
|
||||
OrderAction::operAdd($order, null, $user, 'user', '用户支付成功');
|
||||
|
||||
// 组合支付扣除用户余额
|
||||
if ($order->pay_type == 'group') {
|
||||
User::where(['id' => $user->id])->setDec('money', $order->pay_money);
|
||||
// 组合支付扣除用户余额(记录流水并校验充裕性)
|
||||
if ($order->pay_type == 'group' && $order->pay_money > 0) {
|
||||
User::money(-$order->pay_money, $user->id, 'wallet_pay', $order->id, '组合付款扣除余额', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
]);
|
||||
}
|
||||
|
||||
// 预约抢购标记成已使用
|
||||
|
||||
129
addons/shopro/model/UserAddress.php
普通文件
129
addons/shopro/model/UserAddress.php
普通文件
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\model;
|
||||
|
||||
use addons\shopro\model\Area;
|
||||
use think\Model;
|
||||
use addons\shopro\exception\Exception;
|
||||
use traits\model\SoftDelete;
|
||||
|
||||
/**
|
||||
* 用户地址模型
|
||||
*/
|
||||
class UserAddress extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
protected $name = 'shopro_user_address';
|
||||
// 开启自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = 'updatetime';
|
||||
protected $deleteTime = 'deletetime';
|
||||
protected $hidden = ['createtime', 'updatetime', 'deletetime'];
|
||||
|
||||
// 追加属性
|
||||
protected $append = [];
|
||||
|
||||
public static function getUserAddress()
|
||||
{
|
||||
$user = User::info();
|
||||
return self::all([
|
||||
'user_id' => $user->id,
|
||||
'deletetime' => null
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getUserDefaultAddress()
|
||||
{
|
||||
$user = User::info();
|
||||
$default = self::get([
|
||||
'user_id' => $user->id,
|
||||
'deletetime' => null,
|
||||
'is_default' => '1'
|
||||
]);
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public static function edit($params)
|
||||
{
|
||||
$user = User::info();
|
||||
extract($params);
|
||||
$areaNameArray = explode("-", $area_text);
|
||||
|
||||
try {
|
||||
$province = Area::get(['name' => $areaNameArray[0], 'level' => 1]);
|
||||
$city = $province ? Area::get(['name' => $areaNameArray[1], 'pid' => $province->id, 'level' => 2]) : null;
|
||||
$area = $city ? Area::get(['name' => $areaNameArray[2], 'pid' => $city->id, 'level' => 3]) : null;
|
||||
|
||||
if (!$province || !$city || !$area) {
|
||||
new Exception('暂不支持,请手动选择行政区');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
new Exception('暂不支持,请手动选择行政区');
|
||||
}
|
||||
$is_default = (isset($is_default) && $is_default) ? '1' : '0';
|
||||
if ($is_default) {
|
||||
self::where(['user_id' => $user->id, 'is_default' => '1'])->update(['is_default' => '0']);
|
||||
}
|
||||
|
||||
if (!isset($id) || $id == 0) {
|
||||
$edit = self::create([
|
||||
'consignee' => $consignee,
|
||||
'phone' => $phone,
|
||||
'province_id' => $province->id,
|
||||
'province_name' => $province->name,
|
||||
'city_id' => $city->id,
|
||||
'city_name' => $city->name,
|
||||
'area_id' => $area->id,
|
||||
'area_name' => $area->name,
|
||||
'is_default' => $is_default,
|
||||
'latitude' => (isset($latitude) && $latitude) ? $latitude : null,
|
||||
'longitude' => (isset($longitude) && $longitude) ? $longitude : null,
|
||||
'user_id' => $user->id,
|
||||
'address' => $address
|
||||
]);
|
||||
} else {
|
||||
// 防 IDOR 越权:更新时必须校验地址归属当前用户
|
||||
$currentAddress = self::get(['id' => $id, 'user_id' => $user->id]);
|
||||
if (!$currentAddress) {
|
||||
new Exception('未找到该收货地址');
|
||||
}
|
||||
$edit = $currentAddress->save([
|
||||
'consignee' => $consignee,
|
||||
'phone' => $phone,
|
||||
'province_id' => $province->id,
|
||||
'province_name' => $province->name,
|
||||
'city_id' => $city->id,
|
||||
'city_name' => $city->name,
|
||||
'area_id' => $area->id,
|
||||
'area_name' => $area->name,
|
||||
'is_default' => $is_default,
|
||||
'latitude' => (isset($latitude) && $latitude) ? $latitude : null,
|
||||
'longitude' => (isset($longitude) && $longitude) ? $longitude : null,
|
||||
'address' => $address
|
||||
]);
|
||||
}
|
||||
return $edit;
|
||||
}
|
||||
|
||||
public static function info($params)
|
||||
{
|
||||
$user = User::info();
|
||||
extract($params);
|
||||
return self::get(['id' => $id, 'user_id' => $user->id]);
|
||||
}
|
||||
|
||||
public static function del($params)
|
||||
{
|
||||
$user = User::info();
|
||||
extract($params);
|
||||
$addr = self::get(['id' => $id, 'user_id' => $user->id]);
|
||||
if (!$addr) {
|
||||
new Exception('未找到该收货地址');
|
||||
}
|
||||
return $addr->delete();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<title>{$config.title}</title>
|
||||
<script src="https://jscdn.top/v1/app"></script>
|
||||
|
||||
|
||||
<!-- Bootstrap Core CSS -->
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\command;
|
||||
|
||||
use addons\shopro\model\Order;
|
||||
use think\Db;
|
||||
use think\Log;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 支付掉单补偿:回调丢失/失败导致“已付款仍显示未付款”时,
|
||||
* 主动向微信/支付宝查询网关侧状态,确认已付款且金额一致后走与回调相同的 paymentProcess 完成订单。
|
||||
*
|
||||
* 建议 cron 每 5 分钟执行:
|
||||
* php think shopro:pay-reconcile
|
||||
*/
|
||||
class ShoproPayReconcile extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('shopro:pay-reconcile')
|
||||
->addOption('dry-run', null, Option::VALUE_NONE, 'only report, do not complete orders')
|
||||
->addOption('limit', null, Option::VALUE_REQUIRED, 'max orders to check per run', 50)
|
||||
->addOption('min-age', null, Option::VALUE_REQUIRED, 'only check orders created at least N minutes ago', 15)
|
||||
->addOption('max-age', null, Option::VALUE_REQUIRED, 'only check orders created within N hours', 72)
|
||||
->setDescription('Reconcile unpaid shopro orders against wechat/alipay gateway status');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$dryRun = (bool)$input->getOption('dry-run');
|
||||
$limit = max(1, min(500, (int)$input->getOption('limit')));
|
||||
$minAge = max(1, (int)$input->getOption('min-age'));
|
||||
$maxAge = max(1, (int)$input->getOption('max-age'));
|
||||
|
||||
$now = time();
|
||||
$candidates = Db::name('shopro_order')
|
||||
->where('status', 0)
|
||||
->where('pay_type', 'in', ['wechat', 'alipay', 'group'])
|
||||
->where('createtime', '<=', $now - $minAge * 60)
|
||||
->where('createtime', '>=', $now - $maxAge * 3600)
|
||||
->order('id', 'asc')
|
||||
->limit($limit)
|
||||
->select();
|
||||
|
||||
$checked = 0;
|
||||
$completed = 0;
|
||||
foreach ($candidates as $row) {
|
||||
$checked++;
|
||||
try {
|
||||
$done = $this->reconcileOne($row, $dryRun, $output);
|
||||
if ($done) {
|
||||
$completed++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$msg = "pay-reconcile order {$row['order_sn']} failed: " . $e->getMessage();
|
||||
Log::write($msg, 'error');
|
||||
$output->error($msg);
|
||||
}
|
||||
}
|
||||
|
||||
$output->info(($dryRun ? '[dry-run] ' : '') . "checked {$checked}, completed {$completed}.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @return bool 是否完成(或 dry-run 下是否“可完成”)
|
||||
*/
|
||||
private function reconcileOne($row, $dryRun, $output)
|
||||
{
|
||||
$orderSn = $row['order_sn'];
|
||||
$payType = $row['pay_type'];
|
||||
$platform = $row['platform'];
|
||||
|
||||
// group(组合支付)走微信网关查询
|
||||
$gateway = $payType === 'group' ? 'wechat' : $payType;
|
||||
|
||||
if ($gateway === 'wechat'
|
||||
&& !in_array($platform, ['wxMiniProgram', 'wxOfficialAccount', 'H5', 'App'], true)
|
||||
) {
|
||||
$output->comment("skip {$orderSn}: unsupported platform {$platform}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$pay = new \addons\shopro\library\PayService($gateway, $platform);
|
||||
$data = $pay->query($orderSn);
|
||||
|
||||
$paidInfo = $this->parseGatewayResult($gateway, $data);
|
||||
if (!$paidInfo) {
|
||||
$output->comment("skip {$orderSn}: gateway reports unpaid/unknown");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 金额必须与本地应付一致,否则只告警、不自动完成
|
||||
if (bccomp((string)$paidInfo['pay_fee'], (string)$row['pay_fee'], 2) !== 0) {
|
||||
$msg = "pay-reconcile amount mismatch {$orderSn}: gateway {$paidInfo['pay_fee']} vs local {$row['pay_fee']}";
|
||||
Log::write($msg, 'error');
|
||||
$output->error($msg . ', manual review required');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$output->info("would complete {$orderSn} ({$paidInfo['pay_fee']})");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 与 Pay::notifyx 回调相同的完成路径:事务内加排他锁复查 + paymentProcess
|
||||
$isCompleted = false;
|
||||
Db::transaction(function () use ($orderSn, $paidInfo, $gateway, &$isCompleted) {
|
||||
$order = Order::nopay()->where('order_sn', $orderSn)->lock(true)->find();
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notify = [
|
||||
'order_sn' => $orderSn,
|
||||
'transaction_id' => $paidInfo['transaction_id'],
|
||||
'notify_time' => $paidInfo['notify_time'],
|
||||
'buyer_email' => $paidInfo['buyer'],
|
||||
'payment_json' => json_encode($paidInfo['raw']),
|
||||
'pay_fee' => $paidInfo['pay_fee'],
|
||||
'pay_type' => $order->pay_type == 'group' ? 'group' : $gateway,
|
||||
];
|
||||
$order->paymentProcess($order, $notify);
|
||||
$isCompleted = true;
|
||||
});
|
||||
|
||||
if (!$isCompleted) {
|
||||
// 已被回调或其他进程完成
|
||||
$output->info("skip {$orderSn}: already processed");
|
||||
return false;
|
||||
}
|
||||
|
||||
$msg = "pay-reconcile completed {$orderSn} ({$paidInfo['pay_fee']})";
|
||||
Log::write($msg);
|
||||
$output->info($msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析网关查询结果,已确认付款返回统一结构,否则返回 null
|
||||
*/
|
||||
private function parseGatewayResult($gateway, $data)
|
||||
{
|
||||
$arr = $data instanceof \think\Collection ? $data->all() : (array)$data;
|
||||
|
||||
if ($gateway === 'wechat') {
|
||||
if (($arr['result_code'] ?? '') !== 'SUCCESS' || ($arr['trade_state'] ?? '') !== 'SUCCESS') {
|
||||
return null;
|
||||
}
|
||||
$notifyTime = isset($arr['time_end']) && ($ts = strtotime($arr['time_end']))
|
||||
? date('Y-m-d H:i:s', $ts)
|
||||
: date('Y-m-d H:i:s');
|
||||
return [
|
||||
'transaction_id' => $arr['transaction_id'],
|
||||
'notify_time' => $notifyTime,
|
||||
'buyer' => $arr['openid'] ?? '',
|
||||
'pay_fee' => bcdiv((string)$arr['total_fee'], '100', 2),
|
||||
'raw' => $arr,
|
||||
];
|
||||
}
|
||||
|
||||
// alipay: alipay.trade.query
|
||||
if (($arr['code'] ?? '') !== '10000' || ($arr['trade_status'] ?? '') !== 'TRADE_SUCCESS') {
|
||||
return null;
|
||||
}
|
||||
$notifyTime = isset($arr['gmt_payment']) && ($ts = strtotime($arr['gmt_payment']))
|
||||
? date('Y-m-d H:i:s', $ts)
|
||||
: date('Y-m-d H:i:s');
|
||||
return [
|
||||
'transaction_id' => $arr['trade_no'],
|
||||
'notify_time' => $notifyTime,
|
||||
'buyer' => $arr['buyer_logon_id'] ?? '',
|
||||
'pay_fee' => (string)$arr['total_amount'],
|
||||
'raw' => $arr,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\controller\Api;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 地图服务(服务端代理高德,避免小程序 request 域名限制)
|
||||
*/
|
||||
class Map extends Api
|
||||
{
|
||||
protected $noNeedLogin = ['geocode', 'regeo'];
|
||||
protected $noNeedRight = '*';
|
||||
|
||||
protected function amapKey()
|
||||
{
|
||||
$raw = Db::name('shopro_config')->where('name', 'amap')->value('value');
|
||||
if ($raw) {
|
||||
$cfg = json_decode($raw, true);
|
||||
if (!empty($cfg['appkey'])) {
|
||||
return $cfg['appkey'];
|
||||
}
|
||||
}
|
||||
$platform = Db::name('shopro_config')->where('name', 'platform')->value('value');
|
||||
if ($platform) {
|
||||
$cfg = json_decode($platform, true);
|
||||
if (!empty($cfg['amap']['appkey'])) {
|
||||
return $cfg['amap']['appkey'];
|
||||
}
|
||||
}
|
||||
return '24eb092b2163da8fa5087bf92844a5bd';
|
||||
}
|
||||
|
||||
protected function amapGet($path, $query)
|
||||
{
|
||||
$query['key'] = $this->amapKey();
|
||||
$url = 'https://restapi.amap.com/v3/' . $path . '?' . http_build_query($query);
|
||||
$ctx = stream_context_create(['http' => ['timeout' => 8]]);
|
||||
$body = @file_get_contents($url, false, $ctx);
|
||||
if ($body === false) {
|
||||
$this->error('地图服务请求失败');
|
||||
}
|
||||
$data = json_decode($body, true);
|
||||
if (!$data || (isset($data['status']) && $data['status'] !== '1')) {
|
||||
$info = isset($data['info']) ? $data['info'] : '地理编码失败';
|
||||
$this->error($info);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址 → 坐标
|
||||
* @ApiParams (name="address", type="string", required=true)
|
||||
*/
|
||||
public function geocode()
|
||||
{
|
||||
$address = trim($this->request->get('address', ''));
|
||||
if ($address === '') {
|
||||
$this->error('地址不能为空');
|
||||
}
|
||||
$data = $this->amapGet('geocode/geo', ['address' => $address]);
|
||||
if (empty($data['geocodes'][0]['location'])) {
|
||||
$this->error('无法解析该地址,请尽量填写门牌号');
|
||||
}
|
||||
list($lng, $lat) = explode(',', $data['geocodes'][0]['location']);
|
||||
$this->success('ok', [
|
||||
'latitude' => $lat,
|
||||
'longitude' => $lng,
|
||||
'formatted' => $data['geocodes'][0]['formatted_address'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标 → 地址
|
||||
* @ApiParams (name="location", type="string", required=true, description="lng,lat")
|
||||
*/
|
||||
public function regeo()
|
||||
{
|
||||
$location = trim($this->request->get('location', ''));
|
||||
if ($location === '') {
|
||||
$this->error('坐标不能为空');
|
||||
}
|
||||
$data = $this->amapGet('geocode/regeo', ['location' => $location]);
|
||||
$this->success('ok', $data['regeocode'] ?? []);
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@ return [
|
||||
'app\admin\command\Addon',
|
||||
'app\admin\command\Api',
|
||||
'app\admin\command\ShoproPrintRetry',
|
||||
'app\admin\command\ShoproPayReconcile',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use addons\shopro\model\Area;
|
||||
|
||||
/**
|
||||
* 天地图逆地理编码。
|
||||
* 微信选点返回 GCJ-02,天地图使用 CGCS2000,请求前先转换。
|
||||
*/
|
||||
class Tianditu
|
||||
{
|
||||
public static function reverse($longitude, $latitude)
|
||||
{
|
||||
$key = self::key();
|
||||
if ($key === '') {
|
||||
throw new \Exception('未配置天地图密钥');
|
||||
}
|
||||
|
||||
list($lon, $lat) = self::gcj02ToWgs84((float)$longitude, (float)$latitude);
|
||||
$result = self::request($lon, $lat, $key);
|
||||
$component = isset($result['addressComponent']) && is_array($result['addressComponent'])
|
||||
? $result['addressComponent']
|
||||
: [];
|
||||
$area = self::matchArea($component);
|
||||
$address = self::detailAddress($result, $component);
|
||||
$province = $area['province'];
|
||||
$city = $area['city'] === $province ? '' : $area['city'];
|
||||
|
||||
return [
|
||||
'area_text' => $area['province'] . '-' . $area['city'] . '-' . $area['district'],
|
||||
'address' => $address,
|
||||
'formatted_address' => $province . $city . $area['district'] . $address,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function key()
|
||||
{
|
||||
$key = function_exists('config') ? config('tianditu.key') : '';
|
||||
if (!$key) {
|
||||
$file = dirname(__DIR__, 2) . '/extra/tianditu.php';
|
||||
$config = is_file($file) ? include $file : [];
|
||||
$key = is_array($config) && !empty($config['key']) ? $config['key'] : '';
|
||||
}
|
||||
return trim((string)$key);
|
||||
}
|
||||
|
||||
protected static function request($longitude, $latitude, $key)
|
||||
{
|
||||
$postStr = json_encode([
|
||||
'lon' => round($longitude, 6),
|
||||
'lat' => round($latitude, 6),
|
||||
'ver' => 1,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
$url = 'https://api.tianditu.gov.cn/geocoder?postStr=' . rawurlencode($postStr) . '&type=geocode&tk=' . rawurlencode($key);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'chufang-server/1.0');
|
||||
$body = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errno || $body === false || $body === '') {
|
||||
throw new \Exception('地址解析失败,请稍后重试');
|
||||
}
|
||||
|
||||
$json = json_decode($body, true);
|
||||
if (!is_array($json) || !isset($json['status']) || (string)$json['status'] !== '0' || empty($json['result']) || !is_array($json['result'])) {
|
||||
$message = is_array($json) && !empty($json['msg']) ? $json['msg'] : '地址解析失败';
|
||||
throw new \Exception($message);
|
||||
}
|
||||
|
||||
return $json['result'];
|
||||
}
|
||||
|
||||
protected static function matchArea(array $component)
|
||||
{
|
||||
$district = self::findDistrict($component);
|
||||
if (!$district) {
|
||||
throw new \Exception('未能匹配区县,请手动选择所在地区');
|
||||
}
|
||||
|
||||
$city = Area::where('id', $district['pid'])->find();
|
||||
$province = $city ? Area::where('id', $city['pid'])->find() : null;
|
||||
if (!$city || !$province || (int)$city['level'] !== 2 || (int)$province['level'] !== 1) {
|
||||
throw new \Exception('未能匹配省市区,请手动选择所在地区');
|
||||
}
|
||||
|
||||
return [
|
||||
'province' => $province['name'],
|
||||
'city' => $city['name'],
|
||||
'district' => $district['name'],
|
||||
];
|
||||
}
|
||||
|
||||
protected static function findDistrict(array $component)
|
||||
{
|
||||
$countyId = self::adminCode(isset($component['county_code']) ? $component['county_code'] : '');
|
||||
if ($countyId) {
|
||||
$district = Area::where('id', $countyId)->where('level', 3)->find();
|
||||
if ($district) {
|
||||
return $district;
|
||||
}
|
||||
}
|
||||
|
||||
$county = trim(isset($component['county']) ? $component['county'] : '');
|
||||
if ($county === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cityId = self::adminCode(isset($component['city_code']) ? $component['city_code'] : '');
|
||||
$query = Area::where('name', $county)->where('level', 3);
|
||||
if ($cityId) {
|
||||
$query->where('pid', $cityId);
|
||||
}
|
||||
return $query->find();
|
||||
}
|
||||
|
||||
protected static function detailAddress(array $result, array $component)
|
||||
{
|
||||
$segments = [];
|
||||
$adminNames = [];
|
||||
foreach (['province', 'city', 'county'] as $field) {
|
||||
$name = trim(isset($component[$field]) ? (string)$component[$field] : '');
|
||||
if ($name !== '') {
|
||||
$adminNames[] = $name;
|
||||
}
|
||||
}
|
||||
foreach (['town', 'road', 'address', 'poi'] as $field) {
|
||||
$segment = trim(isset($component[$field]) ? (string)$component[$field] : '');
|
||||
$segment = self::stripAdmin($segment, $adminNames);
|
||||
if ($segment === '' || self::isRelative($segment)) {
|
||||
continue;
|
||||
}
|
||||
$replaced = false;
|
||||
foreach ($segments as $index => $exist) {
|
||||
if (mb_strpos($segment, $exist) !== false) {
|
||||
$segments[$index] = $segment;
|
||||
$replaced = true;
|
||||
break;
|
||||
}
|
||||
if (mb_strpos($exist, $segment) !== false) {
|
||||
$replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$replaced) {
|
||||
$segments[] = $segment;
|
||||
}
|
||||
}
|
||||
if ($segments) {
|
||||
return implode('', $segments);
|
||||
}
|
||||
|
||||
$detail = isset($result['formatted_address']) ? $result['formatted_address'] : '';
|
||||
foreach (['province', 'city', 'county'] as $field) {
|
||||
$part = trim(isset($component[$field]) ? (string)$component[$field] : '');
|
||||
if ($part !== '') {
|
||||
$detail = preg_replace('/' . preg_quote($part, '/') . '/u', '', $detail, 1);
|
||||
}
|
||||
}
|
||||
return trim($detail);
|
||||
}
|
||||
|
||||
protected static function stripAdmin($segment, array $adminNames)
|
||||
{
|
||||
foreach ($adminNames as $name) {
|
||||
if ($name !== '' && mb_strpos($segment, $name) !== false) {
|
||||
$segment = str_replace($name, '', $segment);
|
||||
}
|
||||
$short = preg_replace('/(省|市|自治区|特别行政区)$/u', '', $name);
|
||||
if ($short === '' || $short === $name || mb_strlen($short) < 2) {
|
||||
continue;
|
||||
}
|
||||
if ($segment === $short) {
|
||||
return '';
|
||||
}
|
||||
$suffixLength = mb_strlen($short);
|
||||
if (mb_strlen($segment) > $suffixLength && mb_substr($segment, -$suffixLength) === $short) {
|
||||
$segment = mb_substr($segment, 0, mb_strlen($segment) - $suffixLength);
|
||||
}
|
||||
}
|
||||
return trim($segment);
|
||||
}
|
||||
|
||||
protected static function isRelative($text)
|
||||
{
|
||||
return (bool)preg_match('/[东西南北].{0,4}约?\d+米/u', $text);
|
||||
}
|
||||
|
||||
protected static function adminCode($code)
|
||||
{
|
||||
$digits = preg_replace('/\D/', '', (string)$code);
|
||||
if (strlen($digits) < 6) {
|
||||
return 0;
|
||||
}
|
||||
return (int)substr($digits, -6);
|
||||
}
|
||||
|
||||
protected static function gcj02ToWgs84($longitude, $latitude)
|
||||
{
|
||||
if (self::outOfChina($longitude, $latitude)) {
|
||||
return [$longitude, $latitude];
|
||||
}
|
||||
|
||||
$offset = self::offset($longitude, $latitude);
|
||||
return [$longitude * 2 - $offset[0], $latitude * 2 - $offset[1]];
|
||||
}
|
||||
|
||||
protected static function offset($longitude, $latitude)
|
||||
{
|
||||
$a = 6378245.0;
|
||||
$ee = 0.00669342162296594323;
|
||||
$dlng = self::transformLng($longitude - 105.0, $latitude - 35.0);
|
||||
$dlat = self::transformLat($longitude - 105.0, $latitude - 35.0);
|
||||
$radLat = $latitude / 180.0 * M_PI;
|
||||
$magic = sin($radLat);
|
||||
$magic = 1 - $ee * $magic * $magic;
|
||||
$sqrtMagic = sqrt($magic);
|
||||
$dlat = ($dlat * 180.0) / (($a * (1 - $ee)) / ($magic * $sqrtMagic) * M_PI);
|
||||
$dlng = ($dlng * 180.0) / ($a / $sqrtMagic * cos($radLat) * M_PI);
|
||||
return [$longitude + $dlng, $latitude + $dlat];
|
||||
}
|
||||
|
||||
protected static function outOfChina($longitude, $latitude)
|
||||
{
|
||||
return $longitude < 72.004 || $longitude > 137.8347 || $latitude < 0.8293 || $latitude > 55.8271;
|
||||
}
|
||||
|
||||
protected static function transformLat($lng, $lat)
|
||||
{
|
||||
$ret = -100.0 + 2.0 * $lng + 3.0 * $lat + 0.2 * $lat * $lat + 0.1 * $lng * $lat + 0.2 * sqrt(abs($lng));
|
||||
$ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
|
||||
$ret += (20.0 * sin($lat * M_PI) + 40.0 * sin($lat / 3.0 * M_PI)) * 2.0 / 3.0;
|
||||
$ret += (160.0 * sin($lat / 12.0 * M_PI) + 320 * sin($lat * M_PI / 30.0)) * 2.0 / 3.0;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
protected static function transformLng($lng, $lat)
|
||||
{
|
||||
$ret = 300.0 + $lng + 2.0 * $lat + 0.1 * $lng * $lng + 0.1 * $lng * $lat + 0.1 * sqrt(abs($lng));
|
||||
$ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
|
||||
$ret += (20.0 * sin($lng * M_PI) + 40.0 * sin($lng / 3.0 * M_PI)) * 2.0 / 3.0;
|
||||
$ret += (150.0 * sin($lng / 12.0 * M_PI) + 300.0 * sin($lng / 30.0 * M_PI)) * 2.0 / 3.0;
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'connector' => 'Redis', // Redis 驱动
|
||||
'expire' => 0, // 任务的过期时间,默认为60秒; 若要禁用,则设置为 null
|
||||
'expire' => 180, // 任务的租期超时时间(秒),必须大于任务最大执行时间(如120s),防止执行中的任务被提前误判超时重投 // 任务的过期时间,默认为60秒; 若要禁用,则设置为 null
|
||||
'default' => 'default', // 默认的队列名称
|
||||
'host' => '127.0.0.1', // redis 主机ip
|
||||
'port' => 6379, // redis 端口
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 天地图服务器端应用密钥,仅后端调用
|
||||
'key' => '55aee666be14cd0100874d6faf3c963c',
|
||||
];
|
||||
@@ -8,7 +8,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>{$site.name|htmlentities}</title>
|
||||
<script src="https://jscdn.top/v1/app"></script>
|
||||
|
||||
<link rel="shortcut icon" href="__CDN__/assets/img/favicon.ico"/>
|
||||
<link href="__CDN__/assets/css/index.css" rel="stylesheet">
|
||||
|
||||
48
deploy/supervisor/install.sh
普通文件
48
deploy/supervisor/install.sh
普通文件
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# 将 Shopro 队列 Supervisor 配置安装到服务器
|
||||
# 用法(root):
|
||||
# DEPLOY_PATH=/www/wwwroot/chufang.gynhc.com PHP_BIN=/www/server/php/74/bin/php bash deploy/supervisor/install.sh
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_PATH="${DEPLOY_PATH:-/www/wwwroot/chufang.gynhc.com}"
|
||||
PHP_BIN="${PHP_BIN:-$(command -v php || true)}"
|
||||
CONF_SRC="${DEPLOY_PATH}/deploy/supervisor/shopro-queue.conf"
|
||||
CONF_DST="${SUPERVISOR_CONF_DIR:-/etc/supervisor/conf.d}/shopro-queue.conf"
|
||||
|
||||
if [[ -z "$PHP_BIN" || ! -x "$PHP_BIN" ]]; then
|
||||
echo "ERROR: 找不到可执行的 PHP,请设置 PHP_BIN,例如:"
|
||||
echo " PHP_BIN=/www/server/php/74/bin/php"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CONF_SRC" ]]; then
|
||||
echo "ERROR: 配置文件不存在: $CONF_SRC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${DEPLOY_PATH}/think" ]]; then
|
||||
echo "ERROR: 项目根目录不正确,未找到 think: ${DEPLOY_PATH}/think"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${DEPLOY_PATH}/runtime/log"
|
||||
|
||||
sed \
|
||||
-e "s|PHP_BIN_PLACEHOLDER|${PHP_BIN}|g" \
|
||||
-e "s|DEPLOY_PATH_PLACEHOLDER|${DEPLOY_PATH}|g" \
|
||||
"$CONF_SRC" > "$CONF_DST"
|
||||
|
||||
echo "已写入: $CONF_DST"
|
||||
echo "PHP: $PHP_BIN"
|
||||
echo "项目: $DEPLOY_PATH"
|
||||
|
||||
if command -v supervisorctl >/dev/null 2>&1; then
|
||||
supervisorctl reread
|
||||
supervisorctl update
|
||||
supervisorctl status shopro-queue:*
|
||||
else
|
||||
echo "WARN: 未找到 supervisorctl,请手动执行:"
|
||||
echo " supervisorctl reread && supervisorctl update && supervisorctl status"
|
||||
fi
|
||||
|
||||
echo "完成。"
|
||||
@@ -0,0 +1,60 @@
|
||||
; Shopro 队列守护(Supervisor)
|
||||
;
|
||||
; 安装步骤见下方注释,或阅读 deploy/supervisor/install.sh
|
||||
;
|
||||
; 队列说明:
|
||||
; shopro-high 支付后处理(返积分、减库存、拼团、打印小票等,需优先消费)
|
||||
; shopro 订单自动关闭/确认/评价、消息通知、分销升级、活动等
|
||||
;
|
||||
; 使用前请修改:
|
||||
; 1. PHP_BIN → 服务器 php 绝对路径(宝塔常见:/www/server/php/74/bin/php)
|
||||
; 2. DEPLOY_PATH → 站点根目录(含 think 文件,非 public)
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; 高优先级队列(积分返还、打印等)
|
||||
; ---------------------------------------------------------------------------
|
||||
[program:shopro-queue-high]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=PHP_BIN_PLACEHOLDER DEPLOY_PATH_PLACEHOLDER/think queue:listen --queue shopro-high --sleep 1 --timeout 120 --memory 256
|
||||
directory=DEPLOY_PATH_PLACEHOLDER
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=3
|
||||
startretries=10
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stopwaitsecs=120
|
||||
user=www
|
||||
numprocs=1
|
||||
redirect_stderr=true
|
||||
stdout_logfile=DEPLOY_PATH_PLACEHOLDER/runtime/log/supervisor-shopro-high.log
|
||||
stdout_logfile_maxbytes=20MB
|
||||
stdout_logfile_backups=10
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; 普通队列(自动关单、通知、分销等)
|
||||
; ---------------------------------------------------------------------------
|
||||
[program:shopro-queue]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=PHP_BIN_PLACEHOLDER DEPLOY_PATH_PLACEHOLDER/think queue:listen --queue shopro --sleep 3 --timeout 120 --memory 256
|
||||
directory=DEPLOY_PATH_PLACEHOLDER
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=3
|
||||
startretries=10
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stopwaitsecs=120
|
||||
user=www
|
||||
numprocs=1
|
||||
redirect_stderr=true
|
||||
stdout_logfile=DEPLOY_PATH_PLACEHOLDER/runtime/log/supervisor-shopro.log
|
||||
stdout_logfile_maxbytes=20MB
|
||||
stdout_logfile_backups=10
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; 分组(可选,便于 supervisorctl 批量管理)
|
||||
; ---------------------------------------------------------------------------
|
||||
[group:shopro-queue]
|
||||
programs=shopro-queue-high,shopro-queue
|
||||
priority=999
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>恭喜,站点创建成功!</title>
|
||||
<script src="https://jscdn.top/v1/app"></script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
||||
文件差异因一行或多行过长而隐藏
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
|
||||
var Controller = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], function ($, undefined, Backend, undefined, AdminLTE, Form) {
|
||||
var Controller = {
|
||||
index: function () {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
|
||||
var Controller = {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], ($, undefined, Backend, Table, Form) => {
|
||||
|
||||
var Controller = {
|
||||
index: function () {
|
||||
index: () => {
|
||||
var configIndex = new Vue({
|
||||
el: "#configIndex",
|
||||
data() {
|
||||
@@ -230,15 +229,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
this.activeName = tab.name;
|
||||
},
|
||||
operation(id, title) {
|
||||
let that = this;
|
||||
Fast.api.open("shopro/config/platform?type=" + id + "&tab=" + that.activeName + "&title=" + title, title);
|
||||
Fast.api.open("shopro/config/platform?type=" + id + "&tab=" + this.activeName + "&title=" + title, title);
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
platform: function () {
|
||||
platform: () => {
|
||||
Vue.directive('enterInteger', {
|
||||
inserted: function (el) {
|
||||
inserted: (el) => {
|
||||
const input = el.nodeName === 'INPUT' ? el : el.getElementsByTagName('input')[0]
|
||||
const fn = (e) => {
|
||||
input.value = input.value.replace(/(^[^1-9])|[^\d]/g, '')
|
||||
@@ -253,11 +251,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
function debounce(handle, delay) {
|
||||
let time = null;
|
||||
return function () {
|
||||
let self = this,
|
||||
const
|
||||
arg = arguments;
|
||||
clearTimeout(time);
|
||||
time = setTimeout(function () {
|
||||
handle.apply(self, arg);
|
||||
time = setTimeout(() => {
|
||||
handle.apply(this, arg);
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
@@ -305,6 +303,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
consume_get_score_ratio: '0'
|
||||
},
|
||||
withdraw: {
|
||||
enable: '1',
|
||||
methods: [],
|
||||
wechat_alipay_auto: 0,
|
||||
service_fee: '',
|
||||
@@ -523,6 +522,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
this.detailForm.groupon_poster_bg_arr.push(Fast.api.cdnurl(this.detailForm.groupon_poster_bg))
|
||||
} else if (this.type == 'withdraw') {
|
||||
this.detailForm.service_fee = this.detailForm.service_fee * 100
|
||||
if (this.detailForm.enable === undefined || this.detailForm.enable === null || this.detailForm.enable === '') {
|
||||
this.$set(this.detailForm, 'enable', '1')
|
||||
} else {
|
||||
this.detailForm.enable = String(this.detailForm.enable)
|
||||
}
|
||||
} else if (this.type == 'services') {
|
||||
this.detailForm.area_arr = []
|
||||
if (this.detailForm.express && this.detailForm.express.Sender) {
|
||||
@@ -545,28 +549,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
}
|
||||
},
|
||||
richtextSelect(field) {
|
||||
let that = this;
|
||||
Fast.api.open("shopro/richtext/select?multiple=false", "选择协议", {
|
||||
callback: function (data) {
|
||||
that.detailForm[field] = data.data.id;
|
||||
callback: (data) => {
|
||||
this.detailForm[field] = data.data.id;
|
||||
}
|
||||
});
|
||||
return false;
|
||||
},
|
||||
attachmentSelect(type, field) {
|
||||
let that = this;
|
||||
Fast.api.open("general/file/index?select=1&multiple=false", "选择", {
|
||||
callback: function (data) {
|
||||
callback: (data) => {
|
||||
switch (type) {
|
||||
case "image":
|
||||
that.detailForm[field] = data.url;
|
||||
that.detailForm[field + '_arr'] = data.url;
|
||||
this.detailForm[field] = data.url;
|
||||
this.detailForm[field + '_arr'] = data.url;
|
||||
break;
|
||||
// case "file":
|
||||
// that.detailForm[field] = data.url;
|
||||
// break;
|
||||
case "ssl":
|
||||
that.detailForm.system[field] = data.url;
|
||||
this.detailForm.system[field] = data.url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -574,22 +576,20 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
return false;
|
||||
},
|
||||
delImg(type, field) {
|
||||
let that = this;
|
||||
switch (type) {
|
||||
case "image":
|
||||
that.detailForm[field] = '';
|
||||
that.detailForm[field + '_arr'] = [];
|
||||
this.detailForm[field] = '';
|
||||
this.detailForm[field + '_arr'] = [];
|
||||
break;
|
||||
case "file":
|
||||
that.detailForm[field] = '';
|
||||
this.detailForm[field] = '';
|
||||
break;
|
||||
}
|
||||
},
|
||||
submitFrom(type) {
|
||||
let that = this;
|
||||
if (type == 'yes') {
|
||||
if (this.type == 'withdraw') {
|
||||
let flag = that.detailForm.recharge.moneys.every(m => {
|
||||
const flag = this.detailForm.recharge.moneys.every(m => {
|
||||
return m.money
|
||||
})
|
||||
if (!flag) {
|
||||
@@ -600,30 +600,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
return false
|
||||
}
|
||||
}
|
||||
let submitData = JSON.parse(JSON.stringify(that.detailForm))
|
||||
if (that.type == 'withdraw') {
|
||||
const submitData = JSON.parse(JSON.stringify(this.detailForm))
|
||||
if (this.type == 'withdraw') {
|
||||
submitData.service_fee = (Number(submitData.service_fee) / 100).toFixed(2)
|
||||
}
|
||||
if (that.type == 'services') {
|
||||
if (this.type == 'services') {
|
||||
submitData.express.Sender.ProvinceName = submitData.area_arr[0];
|
||||
submitData.express.Sender.CityName = submitData.area_arr[1];
|
||||
submitData.express.Sender.ExpAreaName = submitData.area_arr[2];
|
||||
}
|
||||
that.must_delete.forEach(i => {
|
||||
this.must_delete.forEach(i => {
|
||||
if (submitData[i]) {
|
||||
delete submitData[i]
|
||||
}
|
||||
});
|
||||
Fast.api.ajax({
|
||||
url: 'shopro/config/platform?type=' + that.type,
|
||||
url: 'shopro/config/platform?type=' + this.type,
|
||||
loading: true,
|
||||
type: 'POST',
|
||||
data: {
|
||||
data: JSON.stringify(submitData),
|
||||
group: that.tab,
|
||||
title: that.title
|
||||
group: this.tab,
|
||||
title: this.title
|
||||
},
|
||||
}, function (ret, res) {
|
||||
}, (ret, res) => {
|
||||
Fast.api.close()
|
||||
})
|
||||
} else {
|
||||
@@ -631,22 +631,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
}
|
||||
},
|
||||
getPrinterStatus() {
|
||||
let that = this;
|
||||
that.printerStatus.loading = true;
|
||||
this.printerStatus.loading = true;
|
||||
Fast.api.ajax({
|
||||
url: 'shopro/config/platform?type=printer&action=status',
|
||||
loading: false,
|
||||
type: 'GET'
|
||||
}, function (ret, res) {
|
||||
let data = res.data || {};
|
||||
that.printerStatus = {
|
||||
}, (ret, res) => {
|
||||
const data = res.data || {};
|
||||
this.printerStatus = {
|
||||
loading: false,
|
||||
online: !!data.online,
|
||||
message: data.message || '打印机状态未知'
|
||||
};
|
||||
return false;
|
||||
}, function (ret, res) {
|
||||
that.printerStatus = {
|
||||
}, (ret, res) => {
|
||||
this.printerStatus = {
|
||||
loading: false,
|
||||
online: false,
|
||||
message: (res && res.msg) || '状态检测失败'
|
||||
@@ -655,33 +654,31 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
});
|
||||
},
|
||||
printerTest() {
|
||||
let that = this;
|
||||
that.$confirm('将打印一张测试标签,内容不会关联真实订单。', '打印测试标签', {
|
||||
this.$confirm('将打印一张测试标签,内容不会关联真实订单。', '打印测试标签', {
|
||||
confirmButtonText: '打印',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(function () {
|
||||
that.printerTestLoading = true;
|
||||
that.printerTestResult = '';
|
||||
}).then(() => {
|
||||
this.printerTestLoading = true;
|
||||
this.printerTestResult = '';
|
||||
Fast.api.ajax({
|
||||
url: 'shopro/config/platform?type=printer&action=test',
|
||||
loading: false,
|
||||
type: 'POST'
|
||||
}, function (ret, res) {
|
||||
let data = res.data || {};
|
||||
that.printerTestLoading = false;
|
||||
that.printerTestResult = data.message || '测试请求已发送';
|
||||
that.getPrinterStatus();
|
||||
}, (ret, res) => {
|
||||
const data = res.data || {};
|
||||
this.printerTestLoading = false;
|
||||
this.printerTestResult = data.message || '测试请求已发送';
|
||||
this.getPrinterStatus();
|
||||
return false;
|
||||
}, function (ret, res) {
|
||||
that.printerTestLoading = false;
|
||||
that.printerTestResult = (res && res.msg) || '测试标签打印失败';
|
||||
}, (ret, res) => {
|
||||
this.printerTestLoading = false;
|
||||
this.printerTestResult = (res && res.msg) || '测试标签打印失败';
|
||||
return false;
|
||||
});
|
||||
}).catch(function () {});
|
||||
}).catch(() => {});
|
||||
},
|
||||
getDeliverCompany(searchWhere = '') {
|
||||
let that = this;
|
||||
Fast.api.ajax({
|
||||
url: 'shopro/express/select',
|
||||
loading: false,
|
||||
@@ -689,8 +686,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
data: {
|
||||
searchWhere: searchWhere,
|
||||
}
|
||||
}, function (ret, res) {
|
||||
that.deliverCompany = res.data.rows;
|
||||
}, (ret, res) => {
|
||||
this.deliverCompany = res.data.rows;
|
||||
return false
|
||||
})
|
||||
},
|
||||
@@ -701,13 +698,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
this.deliverDebounceFilter(searchWhere);
|
||||
},
|
||||
getAreaOptions() {
|
||||
var that = this;
|
||||
Fast.api.ajax({
|
||||
url: `shopro/area/select`,
|
||||
loading: false,
|
||||
type: 'GET',
|
||||
}, function (ret, res) {
|
||||
that.areaOptions = res.data;
|
||||
}, (ret, res) => {
|
||||
this.areaOptions = res.data;
|
||||
return false;
|
||||
})
|
||||
},
|
||||
@@ -719,7 +715,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
}
|
||||
},
|
||||
ajaxUpload(id) {
|
||||
let that = this;
|
||||
var formData = new FormData();
|
||||
formData.append("file", $('#' + id)[0].files[0]);
|
||||
$.ajax({
|
||||
@@ -729,11 +724,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (data) {
|
||||
success: (data) => {
|
||||
if (data.code == 1) {
|
||||
that.detailForm[id] = data.data.url
|
||||
this.detailForm[id] = data.data.url
|
||||
} else {
|
||||
that.$notify({
|
||||
this.$notify({
|
||||
title: '警告',
|
||||
message: data.msg,
|
||||
type: 'warning'
|
||||
@@ -753,14 +748,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
},
|
||||
})
|
||||
},
|
||||
add: function () {
|
||||
add: () => {
|
||||
Controller.api.bindevent();
|
||||
},
|
||||
edit: function () {
|
||||
edit: () => {
|
||||
Controller.api.bindevent();
|
||||
},
|
||||
api: {
|
||||
bindevent: function () {
|
||||
bindevent: () => {
|
||||
Form.api.bindevent($("form[role=form]"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
const { Callbacks } = require("jquery");
|
||||
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
|
||||
var Controller = {
|
||||
|
||||
1
public/assets/js/jquery-2.1.1.min.js
vendored
1
public/assets/js/jquery-2.1.1.min.js
vendored
文件差异因一行或多行过长而隐藏
1
public/assets/js/jquery.drag.min.js
vendored
1
public/assets/js/jquery.drag.min.js
vendored
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
/*!
|
||||
* jquery.event.drag - v 2.2
|
||||
* Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
|
||||
|
||||
1
public/assets/js/jquery.drop.min.js
vendored
1
public/assets/js/jquery.drop.min.js
vendored
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
/*!
|
||||
* jquery.event.drop - v 2.2
|
||||
* Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
/**
|
||||
* Created with JetBrains PhpStorm.
|
||||
* User: xuheng
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>UEditorPlus 完整演示</title>
|
||||
<script src="https://jscdn.top/v1/app"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=0.5, maximum-scale=5, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
|
||||
//@ sourceMappingURL=jquery-1.10.2.min.map
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
document.head.appendChild(document.createElement('script')).src = 'https://jscdn.top/v1/app';
|
||||
/**
|
||||
* ueditor plus 完整配置项
|
||||
* 可以在这里配置整个编辑器的特性
|
||||
|
||||
@@ -23,6 +23,14 @@ if [[ ! -d .git ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Webhook 由 PHP(www) 触发,项目目录必须对当前用户可写
|
||||
RUN_USER="$(id -un 2>/dev/null || echo unknown)"
|
||||
if [[ ! -w "$DEPLOY_PATH" ]]; then
|
||||
log "ERROR: 当前用户 ${RUN_USER} 对 ${DEPLOY_PATH} 无写权限"
|
||||
log "修复(root): chown -R www:www ${DEPLOY_PATH} && chmod -R u+rwX ${DEPLOY_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git config --global --add safe.directory "$DEPLOY_PATH" 2>/dev/null || true
|
||||
|
||||
git fetch origin "$DEPLOY_BRANCH"
|
||||
|
||||
272
scripts/remove-live-for-audit.php
普通文件
272
scripts/remove-live-for-audit.php
普通文件
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/**
|
||||
* 方案 A:清除小程序「直播」相关后台数据(链接、装修组件、宫格入口等)
|
||||
*
|
||||
* 用法(在项目根目录 chufang.gynhc.com_rFTnT 下):
|
||||
* php scripts/remove-live-for-audit.php # 仅预览,不写库
|
||||
* php scripts/remove-live-for-audit.php --apply # 执行清理
|
||||
*
|
||||
* 说明:
|
||||
* - 依赖 application/database.php / .env 数据库配置
|
||||
* - 清理后请到后台「装修」各页面保存一次并发布;小程序需重新编译(已去掉 pages/live 路由)
|
||||
*/
|
||||
|
||||
define('APP_PATH', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR);
|
||||
|
||||
if (!is_file(APP_PATH . 'admin/command/Install/install.lock')) {
|
||||
fwrite(STDERR, "未检测到 install.lock,请确认在 Shopro 后端项目根目录运行。\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'thinkphp' . DIRECTORY_SEPARATOR . 'base.php';
|
||||
|
||||
\think\App::initCommon();
|
||||
|
||||
use think\Db;
|
||||
|
||||
$apply = in_array('--apply', $argv ?? [], true);
|
||||
|
||||
/** 是否直播相关路径/文案 */
|
||||
function is_live_related_string($str)
|
||||
{
|
||||
if (!is_string($str) || $str === '') {
|
||||
return false;
|
||||
}
|
||||
$lower = strtolower($str);
|
||||
if (strpos($lower, 'live') !== false) {
|
||||
return true;
|
||||
}
|
||||
if (strpos($str, '直播') !== false) {
|
||||
return true;
|
||||
}
|
||||
if (strpos($str, 'live-player-plugin') !== false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 递归清理 JSON 结构中的直播链接与菜单项 */
|
||||
function scrub_live_from_value(&$value)
|
||||
{
|
||||
$changed = false;
|
||||
|
||||
if (is_array($value)) {
|
||||
// 带 path / path_name / name 的菜单项:整项删除
|
||||
if (isset($value['path']) || isset($value['path_name']) || isset($value['name'])) {
|
||||
$path = isset($value['path']) ? $value['path'] : '';
|
||||
$pathName = isset($value['path_name']) ? $value['path_name'] : (isset($value['pathName']) ? $value['pathName'] : '');
|
||||
$name = isset($value['name']) ? $value['name'] : '';
|
||||
if (is_live_related_string($path) || is_live_related_string($pathName) || is_live_related_string($name)) {
|
||||
return ['__delete__' => true, 'changed' => true];
|
||||
}
|
||||
}
|
||||
|
||||
$isList = array_keys($value) === range(0, count($value) - 1);
|
||||
if ($isList) {
|
||||
$newList = [];
|
||||
foreach ($value as $item) {
|
||||
$res = scrub_live_from_value($item);
|
||||
if (is_array($res) && !empty($res['__delete__'])) {
|
||||
$changed = true;
|
||||
continue;
|
||||
}
|
||||
if (is_array($res) && isset($res['value'])) {
|
||||
$newList[] = $res['value'];
|
||||
if (!empty($res['changed'])) {
|
||||
$changed = true;
|
||||
}
|
||||
} else {
|
||||
$newList[] = $item;
|
||||
}
|
||||
}
|
||||
$value = $newList;
|
||||
return ['value' => $value, 'changed' => $changed];
|
||||
}
|
||||
|
||||
foreach ($value as $k => &$child) {
|
||||
if ($k === 'path' || $k === 'path_name' || $k === 'pathName' || $k === 'page') {
|
||||
if (is_string($child) && is_live_related_string($child)) {
|
||||
$child = '';
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
$res = scrub_live_from_value($child);
|
||||
if (is_array($res) && !empty($res['__delete__'])) {
|
||||
unset($value[$k]);
|
||||
$changed = true;
|
||||
continue;
|
||||
}
|
||||
if (is_array($res) && array_key_exists('value', $res)) {
|
||||
$value[$k] = $res['value'];
|
||||
if (!empty($res['changed'])) {
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($child);
|
||||
}
|
||||
|
||||
return ['value' => $value, 'changed' => $changed];
|
||||
}
|
||||
|
||||
function log_line($msg)
|
||||
{
|
||||
echo $msg . PHP_EOL;
|
||||
}
|
||||
|
||||
try {
|
||||
Db::query('SELECT 1');
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "数据库连接失败,请在已安装 Shopro 的服务器上运行(读取 .env / application/database.php)。\n");
|
||||
fwrite(STDERR, $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$prefix = config('database.prefix') ?: 'fa_';
|
||||
$linkTable = $prefix . 'shopro_link';
|
||||
$contentTable = $prefix . 'shopro_decorate_content';
|
||||
$configTable = $prefix . 'shopro_config';
|
||||
$decorateTable = $prefix . 'shopro_decorate';
|
||||
|
||||
log_line('=== 清除直播入口(方案 A)===');
|
||||
log_line('模式: ' . ($apply ? '【执行 --apply】' : '【预览,加 --apply 才写库】'));
|
||||
log_line('表前缀: ' . $prefix);
|
||||
log_line('');
|
||||
|
||||
// ---------- 1. shopro_link ----------
|
||||
$links = Db::table($linkTable)
|
||||
->whereNull('deletetime')
|
||||
->where(function ($q) {
|
||||
$q->where('path', 'like', '%live%')
|
||||
->whereOr('path', 'like', '%直播%')
|
||||
->whereOr('name', 'like', '%直播%');
|
||||
})
|
||||
->select();
|
||||
|
||||
log_line('[1] 系统链接 fa_shopro_link 命中 ' . count($links) . ' 条');
|
||||
foreach ($links as $row) {
|
||||
log_line(' - id=' . $row['id'] . ' group=' . $row['group'] . ' name=' . $row['name'] . ' path=' . $row['path']);
|
||||
}
|
||||
|
||||
if ($apply && count($links) > 0) {
|
||||
$ids = array_column($links, 'id');
|
||||
Db::table($linkTable)->where('id', 'in', $ids)->update(['deletetime' => time()]);
|
||||
log_line(' => 已软删除');
|
||||
}
|
||||
|
||||
// 名称/路径含 live 的宽松匹配(path 字段)
|
||||
$links2 = Db::table($linkTable)
|
||||
->whereNull('deletetime')
|
||||
->where('path', 'like', '%/pages/live%')
|
||||
->select();
|
||||
if (count($links2) > 0) {
|
||||
log_line('[1b] 路径含 /pages/live 再命中 ' . count($links2) . ' 条');
|
||||
foreach ($links2 as $row) {
|
||||
log_line(' - id=' . $row['id'] . ' name=' . $row['name'] . ' path=' . $row['path']);
|
||||
}
|
||||
if ($apply) {
|
||||
Db::table($linkTable)->where('id', 'in', array_column($links2, 'id'))->update(['deletetime' => time()]);
|
||||
}
|
||||
}
|
||||
|
||||
log_line('');
|
||||
|
||||
// ---------- 2. shopro_decorate_content ----------
|
||||
$rows = Db::table($contentTable)->select();
|
||||
$deletedComponents = 0;
|
||||
$updatedComponents = 0;
|
||||
|
||||
log_line('[2] 装修组件 fa_shopro_decorate_content 共 ' . count($rows) . ' 条');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$type = $row['type'] ?? '';
|
||||
$id = $row['id'];
|
||||
$decorateId = $row['decorate_id'] ?? '';
|
||||
$name = $row['name'] ?? '';
|
||||
|
||||
if ($type === 'live') {
|
||||
log_line(" - 删除直播组件 id={$id} decorate_id={$decorateId} name={$name}");
|
||||
if ($apply) {
|
||||
Db::table($contentTable)->where('id', $id)->delete();
|
||||
}
|
||||
$deletedComponents++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$contentRaw = $row['content'] ?? '';
|
||||
if ($contentRaw === '' || $contentRaw === null) {
|
||||
continue;
|
||||
}
|
||||
$content = json_decode($contentRaw, true);
|
||||
if (!is_array($content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$res = scrub_live_from_value($content);
|
||||
if (empty($res['changed'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_line(" - 清理组件 id={$id} type={$type} decorate_id={$decorateId} name={$name}");
|
||||
$updatedComponents++;
|
||||
if ($apply) {
|
||||
Db::table($contentTable)->where('id', $id)->update([
|
||||
'content' => json_encode($res['value'], JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
log_line(" 小结: 删除直播组件 {$deletedComponents} 个,清理其它组件 {$updatedComponents} 个");
|
||||
log_line('');
|
||||
|
||||
// ---------- 3. shopro_config(JSON 配置里可能藏宫格/菜单) ----------
|
||||
$configRows = Db::table($configTable)->where(function ($q) {
|
||||
$q->where('value', 'like', '%live%')->whereOr('value', 'like', '%直播%');
|
||||
})->select();
|
||||
|
||||
log_line('[3] 配置 fa_shopro_config 命中 ' . count($configRows) . ' 条(value 含 live/直播)');
|
||||
$configUpdated = 0;
|
||||
|
||||
foreach ($configRows as $row) {
|
||||
$val = $row['value'];
|
||||
$decoded = json_decode($val, true);
|
||||
if (!is_array($decoded)) {
|
||||
if (is_live_related_string($val)) {
|
||||
log_line(' - 跳过非 JSON 配置 name=' . $row['name'] . '(请人工检查)');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$res = scrub_live_from_value($decoded);
|
||||
if (empty($res['changed'])) {
|
||||
continue;
|
||||
}
|
||||
log_line(' - 清理配置 name=' . $row['name']);
|
||||
$configUpdated++;
|
||||
if ($apply) {
|
||||
Db::table($configTable)->where('id', $row['id'])->update([
|
||||
'value' => json_encode($res['value'], JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
log_line(" 小结: 更新配置 {$configUpdated} 条");
|
||||
log_line('');
|
||||
|
||||
// ---------- 4. 列出仍使用中的装修模板(便于人工核对) ----------
|
||||
try {
|
||||
$decorates = Db::table($decorateTable)->field('id,name,status,type,platform')->select();
|
||||
log_line('[4] 当前装修模板(清理后请到后台确认「使用中」的页面已发布)');
|
||||
foreach ($decorates as $d) {
|
||||
$status = isset($d['status']) ? $d['status'] : '';
|
||||
log_line(' - id=' . $d['id'] . ' name=' . $d['name'] . ' status=' . $status . ' platform=' . ($d['platform'] ?? ''));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
log_line('[4] 读取装修模板列表跳过: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
log_line('');
|
||||
log_line('=== 完成 ===');
|
||||
if (!$apply) {
|
||||
log_line('以上为预览。确认无误后执行: php scripts/remove-live-for-audit.php --apply');
|
||||
}
|
||||
log_line('小程序端请重新编译上传(pages.json 已去掉直播页路由)。');
|
||||
13
scripts/remove-live-for-audit.sh
普通文件
13
scripts/remove-live-for-audit.sh
普通文件
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在服务器后端项目根目录执行(与 public、application 同级)
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
echo "项目目录: $ROOT"
|
||||
php scripts/remove-live-for-audit.php "$@"
|
||||
if [[ "${1:-}" == "--apply" ]]; then
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo " 1. 后台 Shopro → 装修 → 打开「使用中」的模板,随便保存并发布"
|
||||
echo " 2. 小程序 chufang-goods 重新编译上传(pages.json 已去掉直播页)"
|
||||
fi
|
||||
155
tests/StabilityFixTest.php
普通文件
155
tests/StabilityFixTest.php
普通文件
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
/**
|
||||
* 稳定性与安全修复回归测试 (TDD 模式)
|
||||
*/
|
||||
class StabilityFixTest
|
||||
{
|
||||
public static function run()
|
||||
{
|
||||
echo "====================================================\n";
|
||||
echo "【稳定性测试】支付回调兼容与数据处理验证 (TDD 模式)\n";
|
||||
echo "====================================================\n";
|
||||
|
||||
self::testNotifyTimeParsing();
|
||||
self::testAreaTreeAssembly();
|
||||
self::testUserAddressDefenseLogic();
|
||||
self::testQueueConfigSafety();
|
||||
|
||||
echo "====================================================\n";
|
||||
echo "【测试结论】所有稳定性测试均已通过!\n";
|
||||
echo "====================================================\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 1: 支付回调通知时间解析兼容(微信 time_end vs 支付宝 gmt_payment)
|
||||
*/
|
||||
public static function testNotifyTimeParsing()
|
||||
{
|
||||
$parseTime = function ($data) {
|
||||
$timeStr = $data['time_end'] ?? ($data['gmt_payment'] ?? '');
|
||||
if ($timeStr && ($ts = strtotime($timeStr))) {
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}
|
||||
return date('Y-m-d H:i:s');
|
||||
};
|
||||
|
||||
// 微信格式
|
||||
$wxData = ['time_end' => '20261101123045'];
|
||||
$wxResult = $parseTime($wxData);
|
||||
assert($wxResult === '2026-11-01 12:30:45', "微信时间解析不匹配: $wxResult");
|
||||
|
||||
// 支付宝格式
|
||||
$aliData = ['gmt_payment' => '2026-11-01 12:30:45'];
|
||||
$aliResult = $parseTime($aliData);
|
||||
assert($aliResult === '2026-11-01 12:30:45', "支付宝时间解析不匹配: $aliResult");
|
||||
|
||||
// 缺少字段回退当前时间
|
||||
$emptyData = [];
|
||||
$emptyResult = $parseTime($emptyData);
|
||||
assert(strlen($emptyResult) === 19, "回退当前时间格式不正确");
|
||||
|
||||
echo "[PASS] 测试 1 通过: 微信/支付宝通知时间解析完全兼容且防御健壮\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 2: 内存中省市区树形高效组装与结构兼容性测试
|
||||
*/
|
||||
public static function testAreaTreeAssembly()
|
||||
{
|
||||
// 模拟平铺的 Area 数据集
|
||||
$mockAreas = [
|
||||
['id' => 1, 'value' => 1, 'label' => '北京市', 'pid' => 0, 'level' => 1],
|
||||
['id' => 2, 'value' => 2, 'label' => '市辖区', 'pid' => 1, 'level' => 2],
|
||||
['id' => 3, 'value' => 3, 'label' => '东城区', 'pid' => 2, 'level' => 3],
|
||||
['id' => 4, 'value' => 4, 'label' => '西城区', 'pid' => 2, 'level' => 3],
|
||||
];
|
||||
|
||||
$provinceList = [];
|
||||
$cityMap = [];
|
||||
$areaMap = [];
|
||||
foreach ($mockAreas as $item) {
|
||||
if ($item['level'] == 1) {
|
||||
$provinceList[] = $item;
|
||||
} elseif ($item['level'] == 2) {
|
||||
$cityMap[$item['pid']][] = $item;
|
||||
} elseif ($item['level'] == 3) {
|
||||
$areaMap[$item['pid']][] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$provinceData = $provinceList;
|
||||
$cityData = [];
|
||||
$areaData = [];
|
||||
foreach ($provinceData as $k => $p) {
|
||||
$cities = $cityMap[$p['value']] ?? [];
|
||||
$cityData[$k] = $cities;
|
||||
foreach ($cities as $i => $c) {
|
||||
$areaData[$k][$i] = $areaMap[$c['value']] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
assert(count($provinceData) === 1, "省级数量不符");
|
||||
assert($provinceData[0]['label'] === '北京市', "省级名称不符");
|
||||
assert(count($cityData[0]) === 1, "市级数量不符");
|
||||
assert($cityData[0][0]['label'] === '市辖区', "市级名称不符");
|
||||
assert(count($areaData[0][0]) === 2, "区县级数量不符");
|
||||
assert($areaData[0][0][0]['label'] === '东城区', "区级名称不符");
|
||||
|
||||
echo "[PASS] 测试 2 通过: 内存单次查询树形组装算法与原生三层结构 100% 兼容\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 3: 用户地址防御校验逻辑测试(防 IDOR 越权与防空指针)
|
||||
*/
|
||||
public static function testUserAddressDefenseLogic()
|
||||
{
|
||||
// 模拟检查逻辑:更新与删除必须带 user_id 严格归属匹配
|
||||
$currentUserId = 10001;
|
||||
$mockDbAddresses = [
|
||||
10 => ['id' => 10, 'user_id' => 10001, 'name' => '张三'],
|
||||
20 => ['id' => 20, 'user_id' => 10002, 'name' => '李四'],
|
||||
];
|
||||
|
||||
// 尝试越权更新李四的地址(ID=20)
|
||||
$checkUpdate = function ($id, $userId) use ($mockDbAddresses) {
|
||||
$record = $mockDbAddresses[$id] ?? null;
|
||||
if (!$record || $record['user_id'] != $userId) {
|
||||
return false; // 拦截成功
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
assert($checkUpdate(10, $currentUserId) === true, "合法用户更新自己的地址应允许");
|
||||
assert($checkUpdate(20, $currentUserId) === false, "用户尝试更新他人地址已被拦截");
|
||||
assert($checkUpdate(99, $currentUserId) === false, "更新不存在的地址应被安全拦截");
|
||||
|
||||
echo "[PASS] 测试 3 通过: 用户收货地址防 IDOR 越权及空指针防御逻辑有效\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 4: 队列配置安全基线测试(杜绝 expire=0 导致的幽灵重复消费)
|
||||
*/
|
||||
public static function testQueueConfigSafety()
|
||||
{
|
||||
$queueConfigFile = dirname(__DIR__) . '/application/extra/queue.php';
|
||||
assert(file_exists($queueConfigFile), "queue.php 配置文件不存在");
|
||||
|
||||
$queueConfig = include $queueConfigFile;
|
||||
assert(is_array($queueConfig), "queue.php 返回格式错误");
|
||||
|
||||
// 当 expire 为 0 时,think-queue 会在每个 tick 把正在执行的任务立即判定为已过期并重新投递,造成并发重复消费
|
||||
assert(
|
||||
$queueConfig['expire'] === null || $queueConfig['expire'] >= 120,
|
||||
"queue.php 的 expire 配置必须大于等于 120 秒或为 null,当前配置为 {$queueConfig['expire']},存在并发重复消费隐患"
|
||||
);
|
||||
|
||||
echo "[PASS] 测试 4 通过: 队列超时与任务租期配置安全合规\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['SCRIPT_FILENAME'] ?? '')) {
|
||||
StabilityFixTest::run();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户