feat: 公共表更名 common/member 前缀 + 插件命令自动加载 + 版本 1.1.1
- 18 张公共表更名(addon/attachment/configure/links/spider_log/spider_stat/sms/notice/ad/task/prop/medal/help/card -> common_*,member_wallets->member_wallet,score_rule/score_log -> member_*,addon_config->common_addonconf),模型全部对齐新表名,Db 直引用清零 - Attachment 模型补 表名绑定,修复富文本上传查 wxapp_attachment 1146 隐患 - install.sql + 迁移 SQL:backend_admin/backend_role delete_at 默认 0,修复软删除(NULL != 0)误过滤导致后台菜单为空 - AppService::boot() 支持插件 info.php 声明 commands 自动注册插件命令(psr-4 自动加载,坏类名自动跳过) - 各插件(haonav/mqttbroker/wxchat/articles/blog/forum 等)字段与配置同步调整 - 框架版本 1.1.0 -> 1.1.1
This commit is contained in:
+14
-14
@@ -18,7 +18,7 @@ use ywxapp\AddonBase;
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Addon extends addon
|
||||
class Addon extends AddonBase
|
||||
{
|
||||
/**
|
||||
* 安装钩子:表由 install.sql 统一创建,这里无需额外处理
|
||||
@@ -33,19 +33,19 @@ class Addon extends addon
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_connection`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_message`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_topic`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_auth`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_acl`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_retain`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_offline`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_stats`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_rule`");
|
||||
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_outbox`");
|
||||
// 清理框架菜单(后端权限 + 前端/会员规则)
|
||||
Db::execute("DELETE FROM wxapp_user_rule WHERE name LIKE 'mqttbroker:%'");
|
||||
Db::execute("DELETE FROM wxapp_admin_power WHERE addon='mqttbroker'");
|
||||
$prefix = Db::getConfig('prefix');
|
||||
$tables = [
|
||||
'mqttbroker_connection', 'mqttbroker_message', 'mqttbroker_topic',
|
||||
'mqttbroker_auth', 'mqttbroker_acl', 'mqttbroker_retain',
|
||||
'mqttbroker_offline', 'mqttbroker_stats', 'mqttbroker_rule',
|
||||
'mqttbroker_outbox',
|
||||
];
|
||||
foreach ($tables as $t) {
|
||||
Db::execute("DROP TABLE IF EXISTS `{$prefix}{$t}`");
|
||||
}
|
||||
// 清理框架菜单(后端权限 + 前端/会员规则),前缀同样动态获取
|
||||
Db::execute("DELETE FROM `{$prefix}user_rule` WHERE name LIKE 'mqttbroker:%'");
|
||||
Db::execute("DELETE FROM `{$prefix}admin_power` WHERE addon='mqttbroker'");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class TestAcl extends Command
|
||||
|
||||
// 插入临时规则(测试后清理)
|
||||
$rows = [
|
||||
['target_type' => 'member', 'target' => 'alice', 'topic' => 'alice/secret', 'access' => 2, 'allow' => 0, 'sort' => 5, 'remark' => 'tmp'],
|
||||
['target_type' => 'user', 'target' => 'alice', 'topic' => 'alice/secret', 'access' => 2, 'allow' => 0, 'sort' => 5, 'remark' => 'tmp'],
|
||||
['target_type' => 'all', 'target' => '', 'topic' => 'secret/#', 'access' => 2, 'allow' => 0, 'sort' => 10, 'remark' => 'tmp'],
|
||||
['target_type' => 'all', 'target' => '', 'topic' => 'public/#', 'access' => 3, 'allow' => 1, 'sort' => 20, 'remark' => 'tmp'],
|
||||
];
|
||||
|
||||
@@ -75,6 +75,22 @@ return [
|
||||
'value' => 'allow',
|
||||
'tip' => '所有规则都未命中时的兜底策略',
|
||||
],
|
||||
[
|
||||
'name' => 'allow_member_login',
|
||||
'title' => '允许框架会员登录',
|
||||
'type' => 'select',
|
||||
'options' => ['0' => '关闭', '1' => '开启'],
|
||||
'value' => '0',
|
||||
'tip' => '开启后,框架 member_user 表的会员可用其账号密码直接连接 Broker(密码算法一致),无需在"认证账号"中重复建号',
|
||||
],
|
||||
[
|
||||
'name' => 'member_is_superuser',
|
||||
'title' => '会员默认超管',
|
||||
'type' => 'select',
|
||||
'options' => ['0' => '否', '1' => '是'],
|
||||
'value' => '0',
|
||||
'tip' => '会员直通连接是否视为超级用户(跳过 ACL);建议保持"否",用 ACL 规则按 %u 隔离',
|
||||
],
|
||||
[
|
||||
'name' => 'max_keepalive',
|
||||
'title' => '最大保活间隔(秒)',
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace addon\mqttbroker\controller\api;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use addon\mqttbroker\service\Store;
|
||||
|
||||
/**
|
||||
* App 端 MQTT API(需框架会员 JWT 登录)
|
||||
* 路由前缀:/mqttbroker/api/...
|
||||
*/
|
||||
class App extends ApiBase
|
||||
{
|
||||
/**
|
||||
* 当前会员账号(Broker 连接用户名通常为 member.account)
|
||||
*/
|
||||
protected function memberAccount(): string
|
||||
{
|
||||
return (string) ($this->auth->model->account ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的设备 / 连接列表
|
||||
* GET /mqttbroker/api/devices
|
||||
*/
|
||||
public function devices()
|
||||
{
|
||||
$account = $this->memberAccount();
|
||||
if ($account === '') {
|
||||
return $this->result->error('无法获取当前会员账号');
|
||||
}
|
||||
$list = Db::name('mqttbroker_connection')
|
||||
->where('username', $account)
|
||||
->order('update_at', 'desc')
|
||||
->limit(200)
|
||||
->field('client_id,username,ip,status,create_at,update_at')
|
||||
->select()
|
||||
->toArray();
|
||||
return $this->result->success(['list' => $list, 'total' => count($list)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的订阅主题列表
|
||||
* GET /mqttbroker/api/subscriptions
|
||||
*/
|
||||
public function subscriptions()
|
||||
{
|
||||
$account = $this->memberAccount();
|
||||
if ($account === '') {
|
||||
return $this->result->error('无法获取当前会员账号');
|
||||
}
|
||||
$clientIds = Db::name('mqttbroker_connection')
|
||||
->where('username', $account)
|
||||
->column('client_id');
|
||||
$list = [];
|
||||
if ($clientIds) {
|
||||
$list = Db::name('mqttbroker_topic')
|
||||
->whereIn('client_id', $clientIds)
|
||||
->order('id', 'desc')
|
||||
->field('client_id,topic,qos,create_at')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
return $this->result->success(['list' => $list, 'total' => count($list)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息(服务端代发,强制命名空间隔离)
|
||||
* POST /mqttbroker/api/publish
|
||||
* param: topic, payload, qos(0/1/2), retain(0/1)
|
||||
*/
|
||||
public function publish()
|
||||
{
|
||||
$account = $this->memberAccount();
|
||||
if ($account === '') {
|
||||
return $this->result->error('无法获取当前会员账号');
|
||||
}
|
||||
$topic = trim((string) $this->request->post('topic', ''));
|
||||
$payload = (string) $this->request->post('payload', '');
|
||||
$qos = (int) $this->request->post('qos', 0);
|
||||
$retain = (int) $this->request->post('retain', 0);
|
||||
|
||||
if ($topic === '' || $topic[0] === '$') {
|
||||
return $this->result->error('主题不能为空,且禁止发布到 $SYS 等系统主题');
|
||||
}
|
||||
// 命名空间隔离:topic 必须以 {username}/ 开头,避免越权发到他人主题
|
||||
$prefix = $account . '/';
|
||||
if (strncmp($topic, $prefix, strlen($prefix)) !== 0) {
|
||||
return $this->result->error("主题必须以 {$prefix} 开头(命名空间隔离)");
|
||||
}
|
||||
if (!in_array($qos, [0, 1, 2], true)) {
|
||||
$qos = 0;
|
||||
}
|
||||
$ok = Store::enqueueManual($topic, $payload, $qos, $retain, 'app');
|
||||
if (!$ok) {
|
||||
return $this->result->error('发布失败,请稍后重试');
|
||||
}
|
||||
return $this->result->success(['topic' => $topic]);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ declare (strict_types = 1);
|
||||
namespace addon\mqttbroker\controller\api;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use addon\mqttbroker\model\Message;
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,7 @@ use addon\mqttbroker\model\Message;
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Mqtt extends ApiController
|
||||
class Mqtt extends ApiBase
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
@@ -26,9 +26,8 @@ use addon\mqttbroker\service\Store;
|
||||
*/
|
||||
class MqttBroker extends BackendBase
|
||||
{
|
||||
// 与 blog 后台一致:开发期放宽登录校验(生产请改回需登录)
|
||||
// 插件后台由 BackendBase 强制登录校验,未登录访问一律拦截
|
||||
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 概览
|
||||
@@ -232,6 +231,14 @@ class MqttBroker extends BackendBase
|
||||
* ========================================================== */
|
||||
|
||||
public function stats()
|
||||
{
|
||||
return View::fetch('admin/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行监控数据接口(供 stats 页面轮询,避免与页面路由冲突)
|
||||
*/
|
||||
public function statsData()
|
||||
{
|
||||
$row = Db::name('mqttbroker_stats')->where('id', 1)->find();
|
||||
if (!$row) {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-07-10 17:47:52
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-17 17:41:29
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\addon\mqttbroker\info.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
return [
|
||||
'name' => 'mqttbroker',
|
||||
@@ -6,8 +15,8 @@ return [
|
||||
'intro' => '基于 Workerman 自研的 MQTT 3.1.1/5.0 Broker(类 EMQX 轻量版):认证/ACL、WebSocket/TLS、保留与离线消息持久化、共享订阅、$SYS 指标、Redis 多进程桥接、实时仪表盘、转发规则(桥接EMQX)、手动发布零依赖出站队列,含后台管理',
|
||||
'author' => '',
|
||||
'website' => '',
|
||||
'version' => '2.1.1',
|
||||
'state' => 0,
|
||||
'version' => '2.1.2',
|
||||
'state' => 1,
|
||||
'url' => '/mqttbroker/backend',
|
||||
'license' => '',
|
||||
'licenseto' => 0,
|
||||
|
||||
@@ -51,6 +51,7 @@ CREATE TABLE IF NOT EXISTS `__PREFIX__mqttbroker_auth` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(191) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||
`password` varchar(191) NOT NULL DEFAULT '' COMMENT '密码(bcrypt哈希,兼容明文)',
|
||||
`member_id` int unsigned NOT NULL DEFAULT '0' COMMENT '归属框架会员ID(0=非会员账号;会员直通时回填对应 member_user.id)',
|
||||
`is_superuser` tinyint(1) NOT NULL DEFAULT '0' COMMENT '超级用户跳过ACL',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1启用 0禁用',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
|
||||
@@ -28,6 +28,7 @@ Route::group('backend', function () {
|
||||
Route::post('deleteAcl', 'backend/MqttBroker/deleteAcl');
|
||||
// 实时监控
|
||||
Route::get('stats', 'backend/MqttBroker/stats');
|
||||
Route::get('statsData', 'backend/MqttBroker/statsData');
|
||||
// 转发规则(规则引擎 / 桥接到 EMQX)
|
||||
Route::get('rule', 'backend/MqttBroker/rule');
|
||||
Route::post('saveRule', 'backend/MqttBroker/saveRule');
|
||||
@@ -38,4 +39,11 @@ Route::group('backend', function () {
|
||||
Route::group('api', function () {
|
||||
Route::get('status', 'api/Mqtt/status');
|
||||
Route::post('publish', 'api/Mqtt/publish');
|
||||
|
||||
// App 端接口(需会员 JWT 登录):/mqttbroker/api/app/...
|
||||
Route::group('app', function () {
|
||||
Route::get('devices', 'api/App/devices');
|
||||
Route::get('subscriptions', 'api/App/subscriptions');
|
||||
Route::post('publish', 'api/App/publish');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,6 +89,12 @@ class Auth
|
||||
`create_at` int DEFAULT NULL,
|
||||
PRIMARY KEY (`id`), KEY `idx_sort` (`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
// 自愈:老表补 member_id 列(框架会员直通用)
|
||||
$prefix = Db::getConfig('prefix');
|
||||
$has = Db::query("SHOW COLUMNS FROM `{$prefix}mqttbroker_auth` LIKE 'member_id'");
|
||||
if (empty($has)) {
|
||||
Db::execute("ALTER TABLE `{$prefix}mqttbroker_auth` ADD `member_id` int unsigned NOT NULL DEFAULT '0' COMMENT '归属框架会员ID(会员直通回填)'");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
@@ -120,7 +126,27 @@ class Auth
|
||||
}
|
||||
|
||||
if (!$acc) {
|
||||
// 无此账号:匿名开时视为普通匿名用户(用户名仅作标识),否则拒绝
|
||||
// 无此账号:尝试框架会员直通(仅当显式开启且禁止匿名,避免被匿名语义绕过)
|
||||
if (!$anonymous && !empty($this->config['allow_member_login'])) {
|
||||
$member = null;
|
||||
try {
|
||||
$member = Db::name('member_user')
|
||||
->where('account', $username)
|
||||
->where('status', 1)
|
||||
->field('id,password')
|
||||
->find();
|
||||
} catch (\Throwable $e) {
|
||||
$member = null;
|
||||
}
|
||||
// 会员密码使用 bcrypt 原生盐(详见 ywxapp\model\MemberUser),
|
||||
// 表无独立 salt 列,直接 verify 即可。
|
||||
if ($member && !empty($member['password'])
|
||||
&& password_verify((string) $password, $member['password'])) {
|
||||
$isSuper = !empty($this->config['member_is_superuser']);
|
||||
return ['ok' => true, 'superuser' => $isSuper, 'reason' => 'member', 'member_id' => $member['id']];
|
||||
}
|
||||
}
|
||||
// 匿名开时视为普通匿名用户(用户名仅作标识),否则拒绝
|
||||
return $anonymous
|
||||
? ['ok' => true, 'superuser' => false, 'reason' => 'anonymous-named']
|
||||
: ['ok' => false, 'superuser' => false, 'reason' => 'account not found'];
|
||||
@@ -170,7 +196,7 @@ class Auth
|
||||
continue;
|
||||
}
|
||||
$type = $r['target_type'];
|
||||
if ($type === 'member' && $r['target'] !== $username) {
|
||||
if ($type === 'user' && $r['target'] !== $username) {
|
||||
continue;
|
||||
}
|
||||
if ($type === 'client' && $r['target'] !== $clientId) {
|
||||
|
||||
@@ -35,3 +35,11 @@ ALTER TABLE `__PREFIX__mqttbroker_rule`
|
||||
|
||||
ALTER TABLE `__PREFIX__mqttbroker_outbox`
|
||||
CHANGE `create_time` `create_at` INT DEFAULT NULL;
|
||||
|
||||
-- 会员直通用:补 member_id 列(兼容已装旧库;重复执行不会报错)
|
||||
SET @db = DATABASE();
|
||||
SET @has = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @db AND TABLE_NAME = '__PREFIX__mqttbroker_auth' AND COLUMN_NAME = 'member_id');
|
||||
SET @sql = IF(@has = 0, 'ALTER TABLE `__PREFIX__mqttbroker_auth` ADD `member_id` int unsigned NOT NULL DEFAULT 0 COMMENT ''归属框架会员ID(会员直通回填)''', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{extend name="layout"}
|
||||
|
||||
<div class="layui-container">
|
||||
<div class="layui-container">
|
||||
<div class="admin-title">ACL 权限规则</div>
|
||||
<div class="layui-text" style="margin-bottom:10px;color:#999;">
|
||||
需在"服务设置"中开启 ACL 才生效。按 sort 升序匹配,命中第一条即决定放行/拒绝;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{extend name="layout"}
|
||||
|
||||
<div class="layui-container">
|
||||
<div class="layui-container">
|
||||
<div class="admin-title">认证账号</div>
|
||||
<div class="layui-text" style="margin-bottom:10px;color:#999;">
|
||||
当"允许匿名连接"关闭时,客户端必须使用此处账号连接;超级用户跳过 ACL 校验。
|
||||
@@ -8,7 +6,7 @@
|
||||
<button class="layui-btn layui-btn-sm" onclick="editAuth()">+ 新增账号</button>
|
||||
<table class="layui-table" style="margin-top:10px;">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>用户名</th><th>超级用户</th><th>状态</th><th>备注</th><th>操作</th></tr>
|
||||
<tr><th>ID</th><th>用户名</th><th>超级用户</th><th>会员</th><th>状态</th><th>备注</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $list as $c}
|
||||
@@ -16,6 +14,7 @@
|
||||
<td>{$c.id}</td>
|
||||
<td>{$c.username}</td>
|
||||
<td>{if $c.is_superuser}<span class="layui-badge layui-bg-orange">是</span>{else}否{/if}</td>
|
||||
<td>{if $c.member_id}<span class="layui-badge layui-bg-blue">M{$c.member_id}</span>{else}<span style="color:#aaa;">—</span>{/if}</td>
|
||||
<td>{if $c.status}<span class="layui-badge layui-bg-green">启用</span>{else}<span class="layui-badge">禁用</span>{/if}</td>
|
||||
<td>{$c.remark}</td>
|
||||
<td>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{extend name="layout"}
|
||||
|
||||
<div class="layui-container">
|
||||
<div class="layui-container">
|
||||
<div class="admin-title">转发规则(规则引擎 / 桥接到 EMQX)</div>
|
||||
<div class="layui-text" style="margin-bottom:10px;color:#999;">
|
||||
匹配"源主题过滤器"的消息,将被转发到外部 MQTT Broker(如 EMQX)。目标主题支持
|
||||
|
||||
@@ -59,7 +59,7 @@ layui.use(['jquery'], function(){
|
||||
return p(h)+':'+p(m)+':'+p(ss);
|
||||
}
|
||||
function load(){
|
||||
$.getJSON('/mqttbroker/backend/stats', function(res){
|
||||
$.getJSON('/mqttbroker/backend/statsData', function(res){
|
||||
if (res.code !== 0 || !res.data) return;
|
||||
var d = res.data;
|
||||
$('#m_online').text(d.db_online != null ? d.db_online : d.clients_online);
|
||||
|
||||
Reference in New Issue
Block a user