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:
ywxapp
2026-08-20 20:19:15 +08:00
parent 303f548664
commit 1d49e6f5ee
395 changed files with 13342 additions and 2012 deletions
+14 -14
View File
@@ -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;
}
}
+1 -1
View File
@@ -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'],
];
+16
View File
@@ -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' => '最大保活间隔(秒)',
+110
View File
@@ -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]);
}
}
+2 -2
View File
@@ -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) {
+11 -2
View File
@@ -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,
+1
View File
@@ -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 '备注',
+8
View File
@@ -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');
});
});
+28 -2
View File
@@ -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) {
+8
View File
@@ -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 -3
View File
@@ -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);