- 创建OTA版本管理表结构,支持版本名称、构建号、APK文件等信息存储 - 实现后台OTA版本管理界面,包含新增、编辑、删除和发布功能 - 开发API接口用于设备版本检查和更新包下载 - 实现版本发布逻辑,自动归档旧版本并计算APK文件哈希值 - 添加强制更新、目标设备白名单和最低版本限制功能 - 集成文件上传和选择组件,支持APK文件管理 - 实现版本状态管理(草稿、已发布、已归档)和权限控制
147 lines
4.7 KiB
PHP
147 lines
4.7 KiB
PHP
<?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,
|
||
];
|
||
}
|
||
} |