Files
leon 88ec58a15a fix(ota): 修复APK下载URL构建中的重复路径前缀问题
- 兼容数据库中可能出现的重复 /uploads/ 前缀
- 更新相对路径参数描述,支持可能含 /uploads/ 前缀的情况
- 移除强制HTTPS协议的冗余注释
- 添加正则表达式去除重复的 /uploads/ 前缀
- 简化路径拼接逻辑,避免重复前缀导致的URL错误
2026-04-10 10:31:06 +08:00

150 lines
4.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\model\OtaVersion;
use think\Exception;
use think\Log;
/**
* OTA 远程升级接口
*
* 无需鉴权,公开接口
*/
class Ota extends Api
{
// 无需登录
protected $noNeedLogin = ['checkVersion'];
// 无需鉴权
protected $noNeedRight = ['*'];
/**
* 检查设备版本更新
*
* GET /api/ota/checkVersion
*
* @param string $imei 设备 Android ID
* @param string $version 当前版本名
* @param string $build 当前构建号
*/
public function checkVersion()
{
$imei = $this->request->get('imei', '');
$version = $this->request->get('version', '');
$build = $this->request->get('build', '');
if (empty($imei)) {
$this->error('设备标识不能为空');
}
// 记录检查日志
Log::info("OTA检查: imei={$imei}, version={$version}, build={$build}");
// 查询最新已发布版本
$latestVersion = OtaVersion::where('status', 'published')
->order('version_code', 'desc')
->find();
if (!$latestVersion) {
$this->error('暂无可用版本');
}
// 转换为整数比较版本号
$localBuild = intval($build);
$serverBuild = intval($latestVersion['version_code']);
// 如果云端版本不高于本地版本,返回无更新
if ($serverBuild <= $localBuild) {
$this->success('已是最新版本', null);
}
// 检查最低版本限制
if (!empty($latestVersion['min_version'])) {
// 简单字符串比较版本名
if (version_compare($version, $latestVersion['min_version'], '<')) {
$this->error('当前版本过低,请先升级到 ' . $latestVersion['min_version']);
}
}
// 检查设备白名单(如果设置了 target_devices
if (!empty($latestVersion['target_devices'])) {
$targets = array_map('trim', explode(',', $latestVersion['target_devices']));
if (!in_array($imei, $targets)) {
$this->error('该设备不在此次升级范围内');
}
}
// 构建返回数据
$apkUrl = $this->buildApkUrl($latestVersion['apk_file']);
$sha256 = $latestVersion['sha256'] ?: '';
// 如果 sha256 为空或 fileSize 为空,尝试从文件计算
if (empty($sha256) || empty($latestVersion['file_size'])) {
$fileInfo = $this->calculateFileInfo($latestVersion['apk_file']);
if ($fileInfo) {
if (empty($sha256)) {
$sha256 = $fileInfo['sha256'];
// 可选:自动更新数据库
// OtaVersion::update(['sha256' => $sha256], ['id' => $latestVersion['id']]);
}
if (empty($latestVersion['file_size'])) {
$latestVersion['file_size'] = $fileInfo['file_size'];
}
}
}
$data = [
'versionName' => $latestVersion['version_name'],
'versionCode' => (int)$latestVersion['version_code'],
'downloadUrl' => $apkUrl,
'updateLog' => $latestVersion['update_log'],
'fileSize' => (int)($latestVersion['file_size'] ?? 0),
'sha256' => $sha256,
'isForceUpdate' => (bool)$latestVersion['is_force_update'],
];
$this->success('发现新版本', $data);
}
/**
* 构建 APK 下载 URL
*
* 客户端要求必须使用 HTTPS 协议,此处强制转换为 HTTPS。
* 兼容数据库中可能出现的重复 /uploads/ 前缀。
*
* @param string $relativePath 相对路径(可能含 /uploads/ 前缀)
* @return string 完整 HTTPS URL
*/
private function buildApkUrl($relativePath)
{
$domain = request()->domain();
// 强制使用 HTTPS
$domain = preg_replace('/^http:\/\//i', 'https://', $domain);
// 去除重复的 /uploads/ 前缀(如 /uploads/uploads/ → /uploads/
$relativePath = preg_replace('#^/?(uploads/)+#', 'uploads/', $relativePath);
return rtrim($domain, '/') . '/' . $relativePath;
}
/**
* 计算 APK 文件的 SHA-256 和文件大小
*
* @param string $relativePath 相对路径
* @return array|null ['sha256' => string, 'file_size' => int]
*/
private function calculateFileInfo($relativePath)
{
$fullPath = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . ltrim($relativePath, '/');
if (!file_exists($fullPath)) {
return null;
}
$fileSize = filesize($fullPath);
$sha256 = hash_file('sha256', $fullPath);
return [
'sha256' => $sha256,
'file_size' => $fileSize,
];
}
}