chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
<?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 addon\appmall\controller\api;
|
||||
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 主框架升级服务 API
|
||||
*
|
||||
* 契约对齐客户端 ywxapp\service\FrameworkService:
|
||||
* - GET /api/framework/version 返回最新版本 + 历史版本(客户端检测更新)
|
||||
* - GET /api/framework/download 下载核心包 zip(可选签名校验)
|
||||
* 原 upgrade 插件功能,已合并进 market(应用服务中心)。
|
||||
*/
|
||||
class Framework
|
||||
{
|
||||
/**
|
||||
* 版本列表 / 最新版本
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
$this->ensureColumns();
|
||||
try {
|
||||
$rows = Db::name('appmall_framework_list')
|
||||
->where('status', 1)
|
||||
->order('id', 'desc')
|
||||
->limit(20)
|
||||
->field('id,version,title,changelog,type,from_version,patch_from,file_path,patch_path,install_path,create_at')
|
||||
->select()
|
||||
->toArray();
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => '读取版本列表失败:' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
$latest = $rows[0] ?? null;
|
||||
$hasFull = !empty($latest['file_path']);
|
||||
$hasPatch = !empty($latest['patch_path']);
|
||||
$hasInstall = !empty($latest['install_path'] ?? '');
|
||||
$patchFrom = $latest['patch_from'] ?? '';
|
||||
if ($patchFrom === '' && $hasPatch) {
|
||||
$patchFrom = $latest['from_version'] ?? '';
|
||||
}
|
||||
|
||||
// 不向客户端泄露物理路径
|
||||
foreach ($rows as &$r) {
|
||||
$r['has_full'] = !empty($r['file_path']);
|
||||
$r['has_patch'] = !empty($r['patch_path']);
|
||||
$r['has_install'] = !empty($r['install_path']);
|
||||
unset($r['file_path'], $r['patch_path'], $r['install_path']);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return json([
|
||||
'code' => 1,
|
||||
'msg' => 'success',
|
||||
'data' => [
|
||||
'version' => $latest['version'] ?? '',
|
||||
'title' => $latest['title'] ?? '',
|
||||
'changelog' => $latest['changelog'] ?? '',
|
||||
// 兼容旧客户端:type 只表达「整包缺失时才是纯补丁」
|
||||
'type' => $hasFull ? 0 : ($hasPatch ? 1 : (int) ($latest['type'] ?? 0)),
|
||||
'from_version'=> $patchFrom,
|
||||
// 新协议:整包与补丁可同版本共存,客户端按自身版本择优
|
||||
'has_full' => $hasFull,
|
||||
'has_patch' => $hasPatch,
|
||||
'has_install' => $hasInstall,
|
||||
'patch_from' => $patchFrom,
|
||||
'release_at' => $latest['create_at'] ?? 0,
|
||||
'history' => $rows,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容已安装库:补齐 type / from_version / patch_* 列,并迁移旧「补丁行」数据
|
||||
*/
|
||||
private function ensureColumns(): void
|
||||
{
|
||||
$table = \ywxapp\model\BaseModel::currentPrefix() . 'appmall_framework_list';
|
||||
try {
|
||||
$defs = [
|
||||
'type' => "tinyint DEFAULT 0 COMMENT '0=整包 1=补丁'",
|
||||
'from_version' => "varchar(20) DEFAULT '' COMMENT '补丁基础版本'",
|
||||
'patch_path' => "varchar(255) DEFAULT '' COMMENT '增量补丁 zip 物理路径'",
|
||||
'patch_hash' => "varchar(64) DEFAULT '' COMMENT '补丁 SHA256 校验值'",
|
||||
'patch_from' => "varchar(20) DEFAULT '' COMMENT '补丁适用的基础版本'",
|
||||
'install_path' => "varchar(255) DEFAULT '' COMMENT '完整安装包 zip 物理路径'",
|
||||
'install_hash' => "varchar(64) DEFAULT '' COMMENT '完整安装包 SHA256 校验值'",
|
||||
];
|
||||
foreach ($defs as $col => $def) {
|
||||
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$col}'");
|
||||
if (empty($cols)) {
|
||||
Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$col}` {$def}");
|
||||
}
|
||||
}
|
||||
Db::execute(
|
||||
"UPDATE `{$table}` SET patch_path = file_path, patch_hash = file_hash, patch_from = from_version,"
|
||||
. " file_path = '', file_hash = ''"
|
||||
. " WHERE type = 1 AND (patch_path = '' OR patch_path IS NULL) AND file_path <> ''"
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载核心包 zip
|
||||
* 客户端 FrameworkService::download() GET ?version= :
|
||||
* - 直接返回 zip 二进制(首字节非 '{')。
|
||||
* 可选签名校验:config('appmall.addon_download_sign') = true 时,
|
||||
* 请求需带 sign=md5('framework'.version.ts.secret) & ts(5 分钟有效期)。
|
||||
*/
|
||||
public function download()
|
||||
{
|
||||
$version = Request::param('version', '');
|
||||
// 选包:type=install/2 下安装包;type=patch/1 下补丁;type=full/0 下整包;缺省整包(缺失时回退补丁/安装包)
|
||||
$typeRaw = (string) Request::param('type', '');
|
||||
$wantPatch = in_array($typeRaw, ['patch', '1'], true);
|
||||
$wantInstall = in_array($typeRaw, ['install', '2'], true);
|
||||
|
||||
if (Config::get('ywxapp.addon_download_sign', false)) {
|
||||
$sign = Request::param('sign', '');
|
||||
$ts = (int) Request::param('ts', 0);
|
||||
if (!$this->verifySign($version, $ts, $sign)) {
|
||||
return json(['code' => 0, 'message' => '签名校验失败']);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行时自愈:已部署的中心站若早于本版本安装,可能缺下载日志表
|
||||
$this->ensureTables();
|
||||
|
||||
$query = Db::name('appmall_framework_list')->where('status', 1);
|
||||
if ($version !== '') {
|
||||
$query->where('version', $version);
|
||||
}
|
||||
$row = $query->order('id', 'desc')->find();
|
||||
if (empty($row)) {
|
||||
return json(['code' => 0, 'message' => '版本包不存在']);
|
||||
}
|
||||
|
||||
if ($wantInstall) {
|
||||
$path = $row['install_path'] ?? '';
|
||||
} elseif ($wantPatch) {
|
||||
$path = $row['patch_path'] ?? '';
|
||||
} else {
|
||||
$path = $row['file_path'] ?? '';
|
||||
// 兼容「仅补丁/仅安装包」版本行:未显式指定时整包缺失则回退
|
||||
if (($path === '' || !is_file($path)) && !empty($row['patch_path'])) {
|
||||
$path = $row['patch_path'];
|
||||
$wantPatch = true;
|
||||
}
|
||||
if (($path === '' || !is_file($path)) && !empty($row['install_path'])) {
|
||||
$path = $row['install_path'];
|
||||
$wantInstall = true;
|
||||
}
|
||||
}
|
||||
if (empty($path) || !is_file($path)) {
|
||||
$msg = $wantInstall ? '该版本无完整安装包或文件缺失'
|
||||
: ($wantPatch ? '该版本无增量补丁或文件缺失' : '版本包不存在或文件缺失');
|
||||
return json(['code' => 0, 'message' => $msg]);
|
||||
}
|
||||
|
||||
// 记录下载次数与日志(列/表缺失时忽略,不影响下载)
|
||||
try {
|
||||
Db::name('appmall_framework_list')->where('id', $row['id'])->inc('download_count')->update();
|
||||
Db::name('appmall_framework_download_log')->insert([
|
||||
'version' => $row['version'],
|
||||
'uid' => (int) Request::param('uid', 0),
|
||||
'ip' => Request::ip(),
|
||||
'create_at' => time(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
$downloadName = 'ywxapp-' . $row['version']
|
||||
. ($wantInstall ? '-install' : ($wantPatch ? '-patch' : '')) . '.zip';
|
||||
|
||||
// 流式下载响应(复用公共类):避免 ThinkPHP download() 内部 file_get_contents 将整文件读入内存,
|
||||
// 改用分块 fread 输出并支持 Range 续传;类内部已 set_time_limit(0)。
|
||||
return new \ywxapp\library\StreamZipResponse($path, $downloadName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时自愈建表:保证 appmall_framework_download_log 存在(早期安装的中心站可能缺失)。
|
||||
*/
|
||||
private function ensureTables(): void
|
||||
{
|
||||
$prefix = \ywxapp\model\BaseModel::currentPrefix();
|
||||
$tables = [
|
||||
'appmall_framework_download_log' => "CREATE TABLE IF NOT EXISTS `{$prefix}appmall_framework_download_log` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`version` varchar(20) NOT NULL COMMENT '下载的版本号',
|
||||
`uid` int DEFAULT 0 COMMENT '下载用户ID(来自客户端)',
|
||||
`ip` varchar(45) DEFAULT '' COMMENT '下载来源IP',
|
||||
`create_at` int DEFAULT 0 COMMENT '下载时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_version` (`version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主框架下载日志表';",
|
||||
];
|
||||
foreach ($tables as $t => $sql) {
|
||||
try {
|
||||
if (empty(Db::query("SHOW TABLES LIKE '{$prefix}{$t}'"))) {
|
||||
Db::execute($sql);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略(如权限不足),由后续业务报错暴露
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载签名校验
|
||||
*/
|
||||
private function verifySign(string $version, int $ts, string $sign): bool
|
||||
{
|
||||
if ($sign === '' || abs(time() - $ts) > 300) {
|
||||
return false;
|
||||
}
|
||||
$secret = Config::get('ywxapp.addon_secret', 'ywxapp-addon-secret-change-me');
|
||||
$expect = md5('framework' . $version . $ts . $secret);
|
||||
return hash_equals($expect, $sign);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user