chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+235
View File
@@ -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) & ts5 分钟有效期)。
*/
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);
}
}
File diff suppressed because it is too large Load Diff