// +---------------------------------------------------------------------- declare(strict_types=1); namespace addon\iot\controller\backend; use think\facade\Db; use think\facade\View; use ywxapp\controller\BackendBase; use addon\iot\service\OtaService; /** * OTA 固件控制器(P4:固件上传 / 下发升级指令 / 进度上报) */ class Ota extends BackendBase { /** * OTA 管理页(固件列表 + 设备多选下发) */ public function ota() { $devices = Db::name('iot_device') ->field('uuid,title,model')->order('id', 'desc')->select()->toArray(); // 先做 JSON_HEX_* 转义(<>&'"→\uXXXX)再注入模板,避免模板默认 htmlentities // 对 json 中的引号做二次转义导致 JS 字面量失效(SyntaxError: Unexpected token '&') View::assign('devicesJson', (string) json_encode($devices, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)); return View::fetch('admin/ota'); } /** * 固件列表(JSON,支持按型号过滤) */ public function otaList() { $model = trim((string) input('get.model', '')); $this->result->success(['list' => OtaService::list($model)]); } /** * 固件列表(layui.table 分页版,按型号过滤) */ public function otaTable() { $model = trim((string) input('get.model', '')); $page = max(1, (int) input('get.page', 1)); $limit = (int) input('get.limit', 10); if ($limit < 1 || $limit > 100) { $limit = 10; } OtaService::ensureTable(); $q = Db::name('iot_ota'); if ($model !== '') { $q->where('model', $model); } $count = (clone $q)->count(); $rows = $q->order('id', 'desc')->page($page, $limit)->select()->toArray(); $this->result->success(['list' => $rows, 'count' => $count]); } /** * 上传并登记固件(保存到 public/upload/ota,自动计算 SHA256) */ public function saveOta() { $model = trim((string) input('post.model', '')); $version = trim((string) input('post.version', '')); $remark = trim((string) input('post.remark', '')); $status = (int) input('post.status', 1); if ($model === '' || $version === '') { $this->result->error('设备型号与版本号必填'); } $file = $this->request->file('file'); if (empty($file)) { // 常见原因:文件超过 php upload_max_filesize / post_max_size 时 $_FILES 为空或仅有错误码 $this->result->error('未收到固件文件(请确认文件未超过服务器上传大小限制)'); } $ext = strtolower((string) pathinfo($file->getOriginalName(), PATHINFO_EXTENSION)); if (!in_array($ext, ['bin', 'zip', 'hex'], true)) { $this->result->error('仅支持 bin / zip / hex 格式固件'); } $hash = hash_file('sha256', $file->getRealPath()); if (!$hash) { $this->result->error('固件文件读取失败'); } $dir = root_path() . 'public' . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR . 'ota'; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $saveName = substr($hash, 0, 16) . '_' . preg_replace('/[^\w.\-]+/', '_', $version) . '_' . preg_replace('/[^\w.\-]+/', '_', $file->getOriginalName()); try { $file->move($dir, $saveName); } catch (\Throwable $e) { $this->result->error('固件保存失败:' . $e->getMessage() . '(若提示超限,请调大 upload_max_filesize / post_max_size)'); } $abs = realpath($dir . DIRECTORY_SEPARATOR . $saveName); if (!$abs || !is_file($abs)) { $this->result->error('固件保存失败'); } try { $id = OtaService::add([ 'model' => $model, 'version' => $version, 'filename' => $file->getOriginalName(), 'file_hash' => hash_file('sha256', $abs), 'size' => (int) filesize($abs), 'url' => rtrim((string) $this->request->domain(), '/') . '/upload/ota/' . rawurlencode($saveName), 'remark' => $remark, 'status' => $status, ]); } catch (\Throwable $e) { @unlink($abs); $this->result->error('登记失败:' . $e->getMessage()); } $this->result->success(['id' => $id]); } /** * 删除固件(连带删除本地上传文件) */ public function deleteOta() { $id = (int) input('post.id', 0); $ota = Db::name('iot_ota')->where('id', $id)->find(); if ($ota) { OtaService::remove($id); if (preg_match('#/upload/ota/([^/]+)$#', (string) $ota['url'], $m)) { $abs = root_path() . 'public' . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR . 'ota' . DIRECTORY_SEPARATOR . rawurldecode($m[1]); if (is_file($abs)) { @unlink($abs); } } } $this->result->success('已删除'); } /** * 启停固件(停用后不可再下发) */ public function toggleOta() { $id = (int) input('post.id', 0); $enabled = (int) input('post.enabled', 1); Db::name('iot_ota')->where('id', $id)->update(['status' => $enabled ? 1 : 0]); $this->result->success('已更新'); } /** * 向选中的设备下发升级指令(离线设备待上线后由 outbox 补发) */ public function publishOta() { $otaId = (int) input('post.ota_id', 0); $uuids = input('post.uuids/a', []); if (!$otaId) { $this->result->error('固件不能为空'); } $uuids = array_values(array_filter((array) $uuids, fn($v) => trim((string) $v) !== '')); if (!$uuids) { $this->result->error('请选择目标设备'); } try { $n = OtaService::publish($otaId, $uuids); } catch (\Throwable $e) { $this->result->error($e->getMessage()); } $this->result->success("已向 {$n} 台设备下发升级指令(离线设备上线后自动补发)"); } }