feat(ota): 新增OTA远程升级功能

- 创建OTA版本管理表结构,支持版本名称、构建号、APK文件等信息存储
- 实现后台OTA版本管理界面,包含新增、编辑、删除和发布功能
- 开发API接口用于设备版本检查和更新包下载
- 实现版本发布逻辑,自动归档旧版本并计算APK文件哈希值
- 添加强制更新、目标设备白名单和最低版本限制功能
- 集成文件上传和选择组件,支持APK文件管理
- 实现版本状态管理(草稿、已发布、已归档)和权限控制
This commit is contained in:
2026-04-09 17:51:34 +08:00
parent ab27bb6bf6
commit 1e22f5b452
11 changed files with 731 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
<?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,
];
}
}