// +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\mqttbroker\controller\backend; use think\facade\Db; use think\facade\View; use ywxapp\controller\BackendBase; use ywxapp\service\AddonService; use addon\mqttbroker\model\Connection; use addon\mqttbroker\model\Message; use addon\mqttbroker\service\Auth; use addon\mqttbroker\service\Store; /** * MqttBroker 类 * * @author ywxapp */ class MqttBroker extends BackendBase { // 与 blog 后台一致:开发期放宽登录校验(生产请改回需登录) protected $noNeedVerify = ['*']; /** * 概览 */ public function index() { $stats = [ 'online' => Connection::where('status', 1)->count(), 'total' => Connection::count(), 'messages' => Message::count(), 'topics' => Db::name('mqttbroker_topic')->count(), ]; $recent = Message::order('id', 'desc')->limit(10)->select(); View::assign('stats', $stats); View::assign('recent', $recent); return View::fetch('admin/index'); } /** * 客户端连接列表 */ public function connections() { $list = Connection::order('id', 'desc')->paginate(20); View::assign('list', $list); return View::fetch('admin/connections'); } /** * 消息日志 */ public function messages() { $topic = input('topic', ''); $query = Message::order('id', 'desc'); if ($topic) { $query->where('topic', 'like', "%{$topic}%"); } $list = $query->paginate(20); View::assign('list', $list); View::assign('topic', $topic); return View::fetch('admin/messages'); } /** * 发布消息表单 */ public function publish() { return View::fetch('admin/publish'); } /** * 执行发布(零外部依赖:写入出站队列,由常驻 Broker 消费后经内部路由投递) */ public function doPublish() { $topic = input('post.topic', ''); $payload = input('post.payload', ''); $qos = (int) input('post.qos', 0); if (!$topic) { $this->result->error('主题不能为空'); } $ok = Store::enqueueManual($topic, $payload, $qos, 0, 'backend'); if ($ok) { $this->result->success('已加入发布队列,Broker 将投递给在线订阅者'); } $this->result->error('发布失败:无法写入出站队列'); } /** * 服务设置 */ public function setting() { $def = include ADDON_PATH . 'mqttbroker' . DIRECTORY_SEPARATOR . 'config.php'; $saved = AddonService::config('mqttbroker'); View::assign('def', $def); View::assign('saved', $saved ?: []); return View::fetch('admin/setting'); } /** * 保存设置 */ public function saveSetting() { $data = input('post.'); AddonService::config('mqttbroker', $data); $this->result->success('保存成功'); } /* ============================================================ * 认证账号管理 * ========================================================== */ public function auth() { $list = Db::name('mqttbroker_auth')->order('id', 'desc')->paginate(20); View::assign('list', $list); return View::fetch('admin/auth'); } public function saveAuth() { $id = (int) input('post.id', 0); $username = trim((string) input('post.username', '')); $password = (string) input('post.password', ''); $isSuper = (int) input('post.is_superuser', 0); $status = (int) input('post.status', 1); $remark = (string) input('post.remark', ''); if ($username === '') { $this->result->error('用户名不能为空'); } $data = [ 'username' => $username, 'is_superuser' => $isSuper ? 1 : 0, 'status' => $status ? 1 : 0, 'remark' => $remark, 'update_at' => time(), ]; // 仅当填写了密码才更新(编辑时留空表示不改) if ($password !== '') { $data['password'] = Auth::hashPassword($password); } try { if ($id > 0) { Db::name('mqttbroker_auth')->where('id', $id)->update($data); } else { if ($password === '') { $this->result->error('新增账号必须设置密码'); } $data['create_at'] = time(); Db::name('mqttbroker_auth')->insert($data); } } catch (\Throwable $e) { $this->result->error('保存失败:' . $e->getMessage()); } $this->result->success('保存成功'); } public function deleteAuth() { $id = (int) input('post.id', 0); Db::name('mqttbroker_auth')->where('id', $id)->delete(); $this->result->success('已删除'); } /* ============================================================ * ACL 规则管理 * ========================================================== */ public function acl() { $list = Db::name('mqttbroker_acl')->order('sort', 'asc')->order('id', 'asc')->paginate(50); View::assign('list', $list); return View::fetch('admin/acl'); } public function saveAcl() { $id = (int) input('post.id', 0); $data = [ 'target_type' => (string) input('post.target_type', 'all'), 'target' => (string) input('post.target', ''), 'topic' => (string) input('post.topic', ''), 'access' => (int) input('post.access', 3), 'allow' => (int) input('post.allow', 1), 'sort' => (int) input('post.sort', 0), 'remark' => (string) input('post.remark', ''), ]; if ($data['topic'] === '') { $this->result->error('主题过滤器不能为空'); } try { if ($id > 0) { Db::name('mqttbroker_acl')->where('id', $id)->update($data); } else { $data['create_at'] = time(); Db::name('mqttbroker_acl')->insert($data); } } catch (\Throwable $e) { $this->result->error('保存失败:' . $e->getMessage()); } $this->result->success('保存成功(最长 30s 后在 Broker 生效)'); } public function deleteAcl() { $id = (int) input('post.id', 0); Db::name('mqttbroker_acl')->where('id', $id)->delete(); $this->result->success('已删除'); } /* ============================================================ * 实时监控仪表盘(轮询 stats 接口) * ========================================================== */ public function stats() { $row = Db::name('mqttbroker_stats')->where('id', 1)->find(); if (!$row) { $row = [ 'uptime' => 0, 'clients_online' => 0, 'clients_total' => 0, 'subscriptions' => 0, 'messages_received' => 0, 'messages_sent' => 0, 'retained' => 0, 'timestamp' => time(), ]; } // 以连接表的实时在线数补全(更贴近 DB 视角的在线状态) try { $row['db_online'] = Connection::where('status', 1)->count(); } catch (\Throwable $e) { $row['db_online'] = $row['clients_online'] ?? 0; } $this->result->success($row); } /* ============================================================ * 转发规则(规则引擎 / 桥接到 EMQX) * ========================================================== */ public function rule() { $list = Db::name('mqttbroker_rule')->order('id', 'desc')->paginate(20); View::assign('list', $list); return View::fetch('admin/rule'); } public function saveRule() { $id = (int) input('post.id', 0); $data = [ 'name' => (string) input('post.name', ''), 'source_filter' => (string) input('post.source_filter', ''), 'target_type' => (string) input('post.target_type', 'mqtt'), 'target_broker' => (string) input('post.target_broker', ''), 'target_clientid' => (string) input('post.target_clientid', ''), 'target_username' => (string) input('post.target_username', ''), 'target_password' => (string) input('post.target_password', ''), 'target_topic' => (string) input('post.target_topic', ''), 'target_qos' => (int) input('post.target_qos', 0), 'enabled' => (int) input('post.enabled', 1), 'remark' => (string) input('post.remark', ''), ]; if ($data['name'] === '' || $data['source_filter'] === '') { $this->result->error('规则名称与源主题过滤器必填'); } if ($data['target_broker'] === '') { $this->result->error('目标 Broker 地址必填'); } try { if ($id > 0) { Db::name('mqttbroker_rule')->where('id', $id)->update($data); } else { $data['create_at'] = time(); Db::name('mqttbroker_rule')->insert($data); } } catch (\Throwable $e) { $this->result->error('保存失败:' . $e->getMessage()); } $this->result->success('保存成功(最多 30s 后在 Broker 生效)'); } public function deleteRule() { $id = (int) input('post.id', 0); Db::name('mqttbroker_rule')->where('id', $id)->delete(); $this->result->success('已删除'); } }