Files
ftng_web/application/api/controller/Ota.php
leon 1e22f5b452 feat(ota): 新增OTA远程升级功能
- 创建OTA版本管理表结构,支持版本名称、构建号、APK文件等信息存储
- 实现后台OTA版本管理界面,包含新增、编辑、删除和发布功能
- 开发API接口用于设备版本检查和更新包下载
- 实现版本发布逻辑,自动归档旧版本并计算APK文件哈希值
- 添加强制更新、目标设备白名单和最低版本限制功能
- 集成文件上传和选择组件,支持APK文件管理
- 实现版本状态管理(草稿、已发布、已归档)和权限控制
2026-04-09 17:51:34 +08:00

147 lines
4.7 KiB
PHP
Raw 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。
*
* @param string $relativePath 相对路径
* @return string 完整 HTTPS URL
*/
private function buildApkUrl($relativePath)
{
$domain = request()->domain();
// 强制使用 HTTPS客户端要求 downloadUrl 必须以 https:// 开头
$domain = preg_replace('/^http:\/\//i', 'https://', $domain);
return rtrim($domain, '/') . '/uploads/' . ltrim($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,
];
}
}