chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,589 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 主框架在线升级客户端服务
|
||||
*
|
||||
* 对接中心站 upgrade 插件(/appmall/api/framework/*):
|
||||
* - checkVersion() 检测是否有新版本
|
||||
* - download() 下载核心包 zip 到本地临时目录
|
||||
* - apply() 备份当前 ywxapp/ 核心 → 解压覆盖 → 更新版本号 → 清缓存
|
||||
*
|
||||
* 核心目录约定为根目录下的 ywxapp/(与 AppService 中 ADDON_PATH 同级)。
|
||||
*
|
||||
* @package ywxapp\service
|
||||
*/
|
||||
class FrameworkService
|
||||
{
|
||||
const CACHE_KEY = 'framework_version_check';
|
||||
const CACHE_EXPIRE = 600;
|
||||
|
||||
/** @var Client|null */
|
||||
private static $httpClient = null;
|
||||
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心目录(ywxapp/)
|
||||
*/
|
||||
public function coreDir(): string
|
||||
{
|
||||
return root_path() . 'ywxapp' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查升级服务器是否有更新的主框架版本
|
||||
*
|
||||
* @return array [
|
||||
* has_update, current, latest, title, changelog, release_at, error
|
||||
* ]
|
||||
*/
|
||||
public function checkVersion(bool $force = false): array
|
||||
{
|
||||
$current = Config::get('ywxapp.version', '1.0.0');
|
||||
$latest = $this->fetchLatest($force);
|
||||
if (empty($latest)) {
|
||||
return [
|
||||
'has_update' => false,
|
||||
'current' => $current,
|
||||
'latest' => $current,
|
||||
'changelog' => '',
|
||||
'error' => '无法连接升级服务器(api_url=' . Config::get('ywxapp.api_url', '') . ')',
|
||||
];
|
||||
}
|
||||
$ver = $latest['version'] ?? $current;
|
||||
$hasUpdate = version_compare($ver, $current, '>');
|
||||
|
||||
// 新协议:整包与增量补丁可同版本共存(has_full/has_patch/patch_from)。
|
||||
// 旧协议兜底:仅有 type/from_version 时,type=0 视为有整包、type=1 视为仅补丁。
|
||||
$legacyType = (int) ($latest['type'] ?? 0);
|
||||
$hasFull = array_key_exists('has_full', $latest ?? []) ? (bool) $latest['has_full'] : ($legacyType === 0);
|
||||
$hasPatch = array_key_exists('has_patch', $latest ?? []) ? (bool) $latest['has_patch'] : ($legacyType === 1);
|
||||
$patchFrom = $latest['patch_from'] ?? ($latest['from_version'] ?? '');
|
||||
|
||||
// 择优:正好停在补丁基础版本 → 用增量补丁省带宽;否则用整包兜底
|
||||
$usePatch = $hasUpdate && $hasPatch && $patchFrom !== '' && $patchFrom === $current;
|
||||
$error = '';
|
||||
if ($hasUpdate && !$usePatch && !$hasFull) {
|
||||
$error = '新版本 ' . $ver . ' 仅提供增量补丁(基于 ' . $patchFrom . '),当前版本 '
|
||||
. $current . ' 不适用,请等待整包发布';
|
||||
}
|
||||
|
||||
return [
|
||||
'has_update' => $hasUpdate,
|
||||
'current' => $current,
|
||||
'latest' => $ver,
|
||||
'type' => $legacyType, // 兼容字段,选包请以 use_patch 为准
|
||||
'from_version' => $patchFrom,
|
||||
'has_full' => $hasFull,
|
||||
'has_patch' => $hasPatch,
|
||||
'patch_from' => $patchFrom,
|
||||
'use_patch' => $usePatch,
|
||||
'title' => $latest['title'] ?? '',
|
||||
'changelog' => $latest['changelog'] ?? '',
|
||||
'release_at' => $latest['release_at'] ?? 0,
|
||||
'error' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取最新版本信息(带缓存)
|
||||
*/
|
||||
protected function fetchLatest(bool $force): ?array
|
||||
{
|
||||
if (!$force) {
|
||||
$cached = Cache::get(self::CACHE_KEY);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
try {
|
||||
$client = $this->getClient();
|
||||
$query = ['query' => ['current' => Config::get('ywxapp.version', '1.0.0')]];
|
||||
// 统一基址约定:/appmall/api/framework/*(注册于插件 route/app.php 的对外 API 分支);
|
||||
// 首次请求失败/响应非法时再重试一次。
|
||||
try {
|
||||
$resp = $client->get('/appmall/api/framework/version', $query);
|
||||
$json = json_decode($resp->getBody()->getContents(), true);
|
||||
} catch (TransferException $e) {
|
||||
$json = null;
|
||||
}
|
||||
if (empty($json) || (int) ($json['code'] ?? 0) !== 1) {
|
||||
$resp = $client->get('/appmall/api/framework/version', $query);
|
||||
$json = json_decode($resp->getBody()->getContents(), true);
|
||||
}
|
||||
if (empty($json) || (int) ($json['code'] ?? 0) !== 1) {
|
||||
return null;
|
||||
}
|
||||
$data = $json['data'] ?? [];
|
||||
Cache::set(self::CACHE_KEY, $data, self::CACHE_EXPIRE);
|
||||
return $data;
|
||||
} catch (TransferException $e) {
|
||||
Log::error('Framework version check failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载指定版本主框架核心包到本地临时文件
|
||||
*
|
||||
* @param string $version 指定版本(留空取最新)
|
||||
* @param string $type 'full'=整包(默认) 'patch'=增量补丁
|
||||
* @return string 本地临时 zip 路径
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function download(string $version = '', string $type = 'full'): string
|
||||
{
|
||||
$isPatch = ($type === 'patch' || $type === '1');
|
||||
$tmpDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR
|
||||
. 'temp' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tmpDir)) {
|
||||
@mkdir($tmpDir, 0755, true);
|
||||
}
|
||||
$tmpFile = $tmpDir . 'ywxapp-' . ($version ?: 'latest') . ($isPatch ? '-patch' : '') . '.zip';
|
||||
|
||||
// 取消客户端脚本执行时间限制,避免大文件下载/解压整体超时(Web 端默认 max_execution_time 通常 30s)
|
||||
set_time_limit(0);
|
||||
|
||||
$client = $this->getClient();
|
||||
$query = ['type' => $isPatch ? 'patch' : 'full'];
|
||||
if ($version !== '') {
|
||||
$query['version'] = $version;
|
||||
}
|
||||
// 下载签名:与服务端 addon_download_sign 开关一致
|
||||
if (Config::get('ywxapp.addon_download_sign', false)) {
|
||||
$ts = time();
|
||||
$secret = Config::get('ywxapp.addon_secret', '');
|
||||
$query['ts'] = $ts;
|
||||
$query['sign'] = md5('framework' . $version . $ts . $secret);
|
||||
}
|
||||
|
||||
// 用 sink 流式写入临时文件(不整包入内存),并放宽超时与读取超时。
|
||||
// 统一基址 /appmall/api/framework/download(插件 route/app.php 对外 API 分支);失败/404 时重试一次。
|
||||
$options = [
|
||||
'query' => $query,
|
||||
'sink' => $tmpFile,
|
||||
'timeout' => 3600,
|
||||
'read_timeout' => 600,
|
||||
];
|
||||
try {
|
||||
$resp = $client->get('/appmall/api/framework/download', $options);
|
||||
} catch (TransferException $e) {
|
||||
$resp = null;
|
||||
}
|
||||
if ($resp === null || $resp->getStatusCode() === 404) {
|
||||
@unlink($tmpFile);
|
||||
$resp = $client->get('/appmall/api/framework/download', $options);
|
||||
}
|
||||
|
||||
// 服务端错误(HTTP >= 400)或返回 JSON 错误时,sink 写入的是错误内容而非 zip
|
||||
if ($resp->getStatusCode() >= 400) {
|
||||
$err = (string) file_get_contents($tmpFile);
|
||||
@unlink($tmpFile);
|
||||
throw new \Exception('下载失败(HTTP ' . $resp->getStatusCode() . '):' . mb_substr($err, 0, 200));
|
||||
}
|
||||
$fh = fopen($tmpFile, 'rb');
|
||||
$first = $fh ? fread($fh, 1) : '';
|
||||
if ($fh) {
|
||||
fclose($fh);
|
||||
}
|
||||
if ($first === '{') {
|
||||
$json = json_decode((string) file_get_contents($tmpFile), true);
|
||||
@unlink($tmpFile);
|
||||
throw new \Exception($json['msg'] ?? ($json['message'] ?? '下载失败'));
|
||||
}
|
||||
|
||||
// 完整性校验:比对 Content-Length 与实际落盘字节数,不一致即判定下载被截断
|
||||
$size = is_file($tmpFile) ? filesize($tmpFile) : 0;
|
||||
$contentLength = (int) $resp->getHeaderLine('Content-Length');
|
||||
if ($size <= 0) {
|
||||
throw new \Exception('下载失败:未获取到文件内容');
|
||||
}
|
||||
if ($contentLength > 0 && $size !== $contentLength) {
|
||||
@unlink($tmpFile);
|
||||
throw new \Exception('下载文件不完整(期望 ' . $contentLength . ' 字节,实际 ' . $size . ' 字节)');
|
||||
}
|
||||
return $tmpFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用升级:备份 → 解压覆盖 → 更新版本号 → 清缓存
|
||||
*
|
||||
* 支持两种包 + 两种范围(由包内容自动识别):
|
||||
* - 范围:包内含顶层 app/config/public/route/extend 任一 → 视为「整站包」,
|
||||
* 解压到项目根目录(root_path());否则视为「核心包」(仅 ywxapp/),
|
||||
* 解压到核心目录并剥离 ywxapp/ 前缀(兼容旧包)。
|
||||
* - 整包(full, $type=0):备份将被覆盖的目录后整体覆盖。
|
||||
* - 增量补丁(patch, $type=1):仅备份将被替换/删除的文件,覆盖变更文件,
|
||||
* 并按包内 delete.list 删除已下线文件(路径相对解压根目录)。
|
||||
*
|
||||
* 安全:运行时/数据目录(runtime/vendor/data/public/uploads)、客户端配置
|
||||
* (config/database.php、config/ywxapp.php、.env、install.lock) 一律禁止覆盖。
|
||||
*
|
||||
* @param string $zipFile 本地核心包路径
|
||||
* @param string $newVersion 目标版本号
|
||||
* @param int|string $type 0/整包 或 1/补丁
|
||||
* @return array ['backup'=>string, 'version'=>string, 'mode'=>string]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function apply(string $zipFile, string $newVersion, $type = 0): array
|
||||
{
|
||||
$coreDir = $this->coreDir();
|
||||
if (!is_dir($coreDir)) {
|
||||
throw new \Exception('核心目录不存在:' . $coreDir);
|
||||
}
|
||||
$isPatch = ($type === 1 || $type === 'patch');
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipFile) !== true) {
|
||||
throw new \Exception('无法打开核心包');
|
||||
}
|
||||
|
||||
// 判定范围:含顶层 app/config/public/route/extend 任一 → 整站包(解压到项目根);
|
||||
// 否则核心包(仅 ywxapp/,解压到核心目录)。需完整扫描以收集全部顶层目录用于备份。
|
||||
$isWholeSite = false;
|
||||
$topDirs = [];
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$e = $zip->getNameIndex($i);
|
||||
if ($e === false) {
|
||||
continue;
|
||||
}
|
||||
$top = explode('/', trim($e, '/'))[0] ?? '';
|
||||
if ($top !== '') {
|
||||
$topDirs[$top] = true;
|
||||
}
|
||||
}
|
||||
$isWholeSite = (bool) array_intersect(array_keys($topDirs), ['app', 'config', 'public', 'route', 'extend']);
|
||||
$baseDir = $isWholeSite ? rtrim(root_path(), '/\\') : (realpath($coreDir) ?: $coreDir);
|
||||
|
||||
// 整包:备份将被覆盖的目录
|
||||
if (!$isPatch) {
|
||||
$backupDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR
|
||||
. 'backup' . DIRECTORY_SEPARATOR . ($isWholeSite ? 'site-' : 'ywxapp-') . date('YmdHis') . DIRECTORY_SEPARATOR;
|
||||
if ($isWholeSite) {
|
||||
// 仅备份包内涉及的顶层目录(如 app/config/extend/ywxapp/public/route),跳过用户上传数据
|
||||
foreach (array_keys($topDirs) as $t) {
|
||||
$src = root_path() . $t;
|
||||
if (is_dir($src)) {
|
||||
$this->copyDirectory($src, $backupDir . $t, ['uploads']);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->copyDirectory($coreDir, $backupDir);
|
||||
}
|
||||
} else {
|
||||
// 补丁:先解析 delete.list(路径相对解压根目录:整站包为项目根,核心包为 ywxapp/)
|
||||
$deleteList = [];
|
||||
foreach (['delete.list', 'ywxapp/delete.list'] as $meta) {
|
||||
$raw = $zip->getFromName($meta);
|
||||
if ($raw !== false) {
|
||||
foreach (explode("\n", $raw) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line !== '' && $line[0] !== '#') {
|
||||
$deleteList[] = $line;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$backupDir = $backupDir ?? (root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR
|
||||
. 'backup' . DIRECTORY_SEPARATOR . 'patch-' . date('YmdHis') . DIRECTORY_SEPARATOR);
|
||||
|
||||
// 解压覆盖(整站包不解前缀;核心包剥离 ywxapp/ 前缀 & 防 Zip Slip)
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entry = $zip->getNameIndex($i);
|
||||
if ($entry === false) {
|
||||
continue;
|
||||
}
|
||||
// 跳过补丁元数据文件
|
||||
$norm = $isWholeSite ? ltrim($entry, '/') : ltrim(preg_replace('#^ywxapp/#', '', $entry), '/');
|
||||
if ($isPatch && in_array($norm, ['delete.list', 'upgrade.json'], true)) {
|
||||
continue;
|
||||
}
|
||||
$rel = $norm;
|
||||
if ($rel === '' || $rel === false) {
|
||||
continue;
|
||||
}
|
||||
// 保护客户端运行/数据/配置:禁止覆盖
|
||||
$segs = explode('/', $rel);
|
||||
$first = $segs[0] ?? '';
|
||||
if (in_array($first, ['runtime', 'vendor', 'data'], true)
|
||||
|| $rel === '.env' || $rel === 'install.lock'
|
||||
|| ($first === 'public' && ($segs[1] ?? '') === 'uploads')
|
||||
|| ($first === 'config' && in_array($segs[1] ?? '', ['database.php', 'ywxapp.php'], true))) {
|
||||
continue;
|
||||
}
|
||||
if (substr($rel, -1) === '/') {
|
||||
@mkdir($baseDir . DIRECTORY_SEPARATOR . rtrim($rel, '/'), 0755, true);
|
||||
continue;
|
||||
}
|
||||
$target = $baseDir . DIRECTORY_SEPARATOR . $rel;
|
||||
$realBase = realpath(dirname($target)) ?: dirname($target);
|
||||
if (strpos(str_replace('\\', '/', $realBase), str_replace('\\', '/', $baseDir)) !== 0) {
|
||||
$zip->close();
|
||||
throw new \Exception('非法压缩包路径: ' . $entry);
|
||||
}
|
||||
// 补丁:仅备份将被替换的文件
|
||||
if ($isPatch && is_file($target)) {
|
||||
$this->backupSingle($target, $backupDir, $baseDir);
|
||||
}
|
||||
$content = $zip->getFromIndex($i);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
if (!is_dir(dirname($target))) {
|
||||
@mkdir(dirname($target), 0755, true);
|
||||
}
|
||||
file_put_contents($target, $content);
|
||||
}
|
||||
|
||||
// 补丁:删除已下线文件(先备份)
|
||||
if ($isPatch && !empty($deleteList)) {
|
||||
foreach ($deleteList as $rel) {
|
||||
$target = $baseDir . DIRECTORY_SEPARATOR . ltrim($rel, '/');
|
||||
if (is_file($target)) {
|
||||
$this->backupSingle($target, $backupDir, $baseDir);
|
||||
@unlink($target);
|
||||
}
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
// 数据库升级 SQL(包内 upgrade.sql,幂等执行,避免重复变更)
|
||||
$this->applyUpgradeSql($newVersion);
|
||||
|
||||
// 更新版本号(写入 config/ywxapp.php)
|
||||
$this->updateConfigVersion($newVersion);
|
||||
|
||||
// 清缓存
|
||||
try {
|
||||
\ywxapp\service\AppService::clearAddonCache();
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return [
|
||||
'backup' => $backupDir,
|
||||
'version' => $newVersion,
|
||||
'mode' => $isPatch ? 'patch' : 'full',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 单文件备份(补丁模式用)
|
||||
*/
|
||||
protected function backupSingle(string $file, string $backupDir, string $baseDir = ''): void
|
||||
{
|
||||
$ref = $baseDir !== '' ? rtrim($baseDir, '/\\') : rtrim($this->coreDir(), '/\\');
|
||||
$rel = ltrim(substr($file, strlen($ref)), DIRECTORY_SEPARATOR);
|
||||
$dest = rtrim($backupDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $rel;
|
||||
if (!is_dir(dirname($dest))) {
|
||||
@mkdir(dirname($dest), 0755, true);
|
||||
}
|
||||
@copy($file, $dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录(用于备份),可按文件/目录 basename 排除(如 uploads)
|
||||
*/
|
||||
protected function copyDirectory(string $src, string $dst, array $excludeNames = []): void
|
||||
{
|
||||
$src = rtrim($src, '/\\');
|
||||
if (!is_dir($dst)) {
|
||||
@mkdir($dst, 0755, true);
|
||||
}
|
||||
$it = new \RecursiveIteratorIterator(
|
||||
new \RecursiveCallbackFilterIterator(
|
||||
new \RecursiveDirectoryIterator($src, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
function ($file) use ($excludeNames) {
|
||||
return !in_array($file->getFilename(), $excludeNames, true);
|
||||
}
|
||||
),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($it as $item) {
|
||||
$target = $dst . DIRECTORY_SEPARATOR . $it->getSubPathName();
|
||||
if ($item->isDir()) {
|
||||
if (!is_dir($target)) {
|
||||
@mkdir($target, 0755, true);
|
||||
}
|
||||
} else {
|
||||
if (!is_dir(dirname($target))) {
|
||||
@mkdir(dirname($target), 0755, true);
|
||||
}
|
||||
copy($item->getRealPath(), $target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 config/ywxapp.php 的 version 值
|
||||
*/
|
||||
protected function updateConfigVersion(string $version): void
|
||||
{
|
||||
$file = config_path() . 'ywxapp.php';
|
||||
if (!is_file($file)) {
|
||||
return;
|
||||
}
|
||||
$content = file_get_contents($file);
|
||||
$content = preg_replace(
|
||||
"/(['\"]version['\"]\s*=>\s*['\"])[^'\"]*(['\"])/",
|
||||
'${1}' . $version . '${2}',
|
||||
$content
|
||||
);
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库升级 SQL(框架在线升级的数据库变更能力)
|
||||
*
|
||||
* 约定:升级包内放置 ywxapp/upgrade.sql(整站包解压到项目根、核心包解压到 ywxapp/,
|
||||
* 两者最终都位于核心目录 ywxapp/upgrade.sql 下)。apply() 解压完成后自动检测并执行。
|
||||
* - 按语句拆分(忽略 -- 行注释与 /* *\/ 块注释)
|
||||
* - 幂等:用 runtime/framework/sql_applied.json 记录已执行版本,重复升级不重复执行
|
||||
* - 执行失败抛异常,由上层升级流程回滚/提示(文件已备份)
|
||||
*
|
||||
* @param string $newVersion 目标版本号(作为幂等键的一部分)
|
||||
*/
|
||||
protected function applyUpgradeSql(string $newVersion): void
|
||||
{
|
||||
$candidates = [
|
||||
$this->coreDir() . 'upgrade.sql',
|
||||
root_path() . 'upgrade.sql',
|
||||
];
|
||||
$sqlFile = '';
|
||||
foreach ($candidates as $c) {
|
||||
if (is_file($c)) {
|
||||
$sqlFile = $c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($sqlFile === '') {
|
||||
return;
|
||||
}
|
||||
// 幂等:同版本已应用则跳过
|
||||
$applied = $this->loadAppliedSql();
|
||||
$key = $newVersion . ':' . basename($sqlFile);
|
||||
if (isset($applied[$key])) {
|
||||
@unlink($sqlFile);
|
||||
return;
|
||||
}
|
||||
|
||||
$statements = $this->splitSql((string) file_get_contents($sqlFile));
|
||||
if (empty($statements)) {
|
||||
@unlink($sqlFile);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Db::connect();
|
||||
foreach ($statements as $sql) {
|
||||
$db->execute($sql);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Framework upgrade SQL failed: ' . $e->getMessage());
|
||||
throw new \Exception('数据库升级失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
$applied[$key] = date('Y-m-d H:i:s');
|
||||
$this->saveAppliedSql($applied);
|
||||
@unlink($sqlFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SQL 文本按语句拆分为数组(忽略 -- 行注释与 /* *\/ 块注释)
|
||||
*/
|
||||
protected function splitSql(string $sql): array
|
||||
{
|
||||
$sql = preg_replace('#--[^\n]*#', '', $sql);
|
||||
$sql = preg_replace('#/\*.*?\*/#s', '', $sql);
|
||||
$parts = preg_split('/;\s*$/m', $sql);
|
||||
$out = [];
|
||||
foreach ($parts as $p) {
|
||||
$p = trim($p);
|
||||
if ($p !== '') {
|
||||
$out[] = $p;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已执行的升级 SQL 记录文件(runtime/framework/sql_applied.json)
|
||||
*/
|
||||
protected function appliedSqlFile(): string
|
||||
{
|
||||
return root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'sql_applied.json';
|
||||
}
|
||||
|
||||
protected function loadAppliedSql(): array
|
||||
{
|
||||
$f = $this->appliedSqlFile();
|
||||
if (!is_file($f)) {
|
||||
return [];
|
||||
}
|
||||
$a = json_decode((string) file_get_contents($f), true);
|
||||
return is_array($a) ? $a : [];
|
||||
}
|
||||
|
||||
protected function saveAppliedSql(array $applied): void
|
||||
{
|
||||
$f = $this->appliedSqlFile();
|
||||
if (!is_dir(dirname($f))) {
|
||||
@mkdir(dirname($f), 0755, true);
|
||||
}
|
||||
file_put_contents($f, json_encode($applied, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 客户端(与 AddonService 共用 api_url / ssl_verify 配置)
|
||||
*/
|
||||
protected function getClient(): Client
|
||||
{
|
||||
if (self::$httpClient === null) {
|
||||
self::$httpClient = new Client([
|
||||
'base_uri' => rtrim(Config::get('ywxapp.api_url', 'https://api.ywxapp.cn'), '/'),
|
||||
'timeout' => 60,
|
||||
'connect_timeout' => 30,
|
||||
'verify' => (bool) Config::get('appmall.ssl_verify', true),
|
||||
'http_errors' => false,
|
||||
'headers' => [
|
||||
'X-REQUESTED-WITH' => 'XMLHttpRequest',
|
||||
'User-Agent' => 'YwxappFramework/' . (Config::get('ywxapp.version') ?? '1.0.0'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
return self::$httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 HTTP 客户端(配置变更后调用)
|
||||
*/
|
||||
public static function resetHttpClient(): void
|
||||
{
|
||||
self::$httpClient = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user