chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace addon\mqttbroker\service;
use think\facade\Db;
/**
* MQTT 持久化服务:保留消息 + 离线消息队列
*
* - 保留消息(retained)broker 重启后从 DB 恢复,保证新订阅者仍能收到最后一条。
* - 离线消息(offline)clean=false 会话离线期间投递给它的 QoS1/2 消息入库,
* 客户端重连后取出补投。
*
* 均为二进制安全(longblob)。所有操作 try/catchDB 不可用不影响 broker 运行。
* 由 persist_retain / persist_offline 配置开关控制。
*/
class Store
{
/** @var bool 启用保留消息持久化 */
protected $persistRetain;
/** @var bool 启用离线消息持久化 */
protected $persistOffline;
/** @var int 单客户端离线消息上限,超出丢弃最旧 */
protected $offlineLimit;
public function __construct(array $config)
{
$this->persistRetain = (string) ($config['persist_retain'] ?? '1') === '1';
$this->persistOffline = (string) ($config['persist_offline'] ?? '1') === '1';
$this->offlineLimit = (int) ($config['offline_limit'] ?? 1000);
}
public function ensureTables(): void
{
try {
Db::execute("CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_retain` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`topic` varchar(255) NOT NULL DEFAULT '',
`payload` longblob,
`qos` tinyint(1) NOT NULL DEFAULT '0',
`update_at` int DEFAULT NULL,
PRIMARY KEY (`id`), UNIQUE KEY `uk_topic` (`topic`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
Db::execute("CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_offline` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`client_id` varchar(191) NOT NULL DEFAULT '',
`topic` varchar(255) NOT NULL DEFAULT '',
`payload` longblob,
`qos` tinyint(1) NOT NULL DEFAULT '0',
`create_at` int DEFAULT NULL,
PRIMARY KEY (`id`), KEY `idx_client_id` (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
Db::execute("CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_outbox` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`topic` varchar(255) NOT NULL DEFAULT '',
`payload` longblob,
`qos` tinyint(1) NOT NULL DEFAULT '0',
`retain` tinyint(1) NOT NULL DEFAULT '0',
`source` varchar(32) DEFAULT 'manual',
`create_at` int DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待投递 1已投递',
PRIMARY KEY (`id`), KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch (\Throwable $e) {
}
}
/* ---------------- 手动发布出站队列 ---------------- */
/**
* 入队一条手动发布(后台/API 发布经 DB 出栈,Broker 消费后内部路由,零外部依赖)
*/
public static function enqueueManual(string $topic, string $payload, int $qos, int $retain, string $source): bool
{
try {
Db::name('mqttbroker_outbox')->insert([
'topic' => $topic,
'payload' => $payload,
'qos' => $qos,
'retain' => $retain ? 1 : 0,
'source' => $source,
'create_at' => time(),
'status' => 0,
]);
return true;
} catch (\Throwable $e) {
return false;
}
}
/**
* 取出待投递的出站消息(status=0)
* @return array
*/
public function popOutbox(int $limit = 100): array
{
$out = [];
try {
$out = Db::name('mqttbroker_outbox')->where('status', 0)
->order('id', 'asc')->limit($limit)->select()->toArray();
} catch (\Throwable $e) {
}
return $out;
}
/**
* 标记出站消息已投递(避免重复路由)
*/
public function markOutboxSent(array $ids): void
{
if (empty($ids)) {
return;
}
try {
Db::name('mqttbroker_outbox')->whereIn('id', $ids)->update(['status' => 1]);
} catch (\Throwable $e) {
}
}
/* ---------------- 保留消息 ---------------- */
/**
* 启动时加载全部保留消息
* @return array topic => ['payload'=>string,'qos'=>int]
*/
public function loadRetained(): array
{
if (!$this->persistRetain) {
return [];
}
$out = [];
try {
foreach (Db::name('mqttbroker_retain')->select() as $row) {
$out[$row['topic']] = ['payload' => (string) $row['payload'], 'qos' => (int) $row['qos']];
}
} catch (\Throwable $e) {
}
return $out;
}
public function saveRetained(string $topic, string $payload, int $qos): void
{
if (!$this->persistRetain) {
return;
}
try {
$exists = Db::name('mqttbroker_retain')->where('topic', $topic)->find();
if ($exists) {
Db::name('mqttbroker_retain')->where('id', $exists['id'])
->update(['payload' => $payload, 'qos' => $qos, 'update_at' => time()]);
} else {
Db::name('mqttbroker_retain')->insert([
'topic' => $topic, 'payload' => $payload, 'qos' => $qos, 'update_at' => time(),
]);
}
} catch (\Throwable $e) {
}
}
public function deleteRetained(string $topic): void
{
if (!$this->persistRetain) {
return;
}
try {
Db::name('mqttbroker_retain')->where('topic', $topic)->delete();
} catch (\Throwable $e) {
}
}
/* ---------------- 离线消息 ---------------- */
public function queueOffline(string $clientId, string $topic, string $payload, int $qos): void
{
if (!$this->persistOffline) {
return;
}
try {
Db::name('mqttbroker_offline')->insert([
'client_id' => $clientId, 'topic' => $topic,
'payload' => $payload, 'qos' => $qos, 'create_at' => time(),
]);
// 超限裁剪:保留最新 offlineLimit 条
$count = Db::name('mqttbroker_offline')->where('client_id', $clientId)->count();
if ($count > $this->offlineLimit) {
$ids = Db::name('mqttbroker_offline')->where('client_id', $clientId)
->order('id', 'asc')->limit($count - $this->offlineLimit)->column('id');
if ($ids) {
Db::name('mqttbroker_offline')->whereIn('id', $ids)->delete();
}
}
} catch (\Throwable $e) {
}
}
/**
* 取出并清空某客户端的离线消息
* @return array<int,array{topic:string,payload:string,qos:int}>
*/
public function popOffline(string $clientId): array
{
if (!$this->persistOffline) {
return [];
}
$out = [];
try {
$rows = Db::name('mqttbroker_offline')->where('client_id', $clientId)->order('id', 'asc')->select();
foreach ($rows as $r) {
$out[] = ['topic' => $r['topic'], 'payload' => (string) $r['payload'], 'qos' => (int) $r['qos']];
}
if (!empty($out)) {
Db::name('mqttbroker_offline')->where('client_id', $clientId)->delete();
}
} catch (\Throwable $e) {
}
return $out;
}
}