Compare commits

24 Commits

Author SHA1 Message Date
7343267d3c Merge branch 'release/1.2.1' 2025-05-15 17:53:37 +08:00
9028a8c59d 1.2.1 2025-05-15 17:53:26 +08:00
daefbe884c 增加用户的过期时间 2025-05-15 17:52:55 +08:00
9ad385e044 Merge branch 'release/1.1.2' 2025-05-15 16:34:04 +08:00
9b926634cf Merge tag '1.1.2' into develop 2025-05-15 16:34:04 +08:00
320739411a 1.1.2 2025-05-15 16:33:52 +08:00
cffc90dfc9 更新创建和修改参数 2025-05-15 16:32:42 +08:00
5b84406652 Merge branch 'release/1.1.1' 2025-05-13 23:49:39 +08:00
3c5020b80b Merge tag '1.1.1' into develop 2025-05-13 23:49:39 +08:00
e50eed84a2 1.1.1 2025-05-13 23:49:29 +08:00
41da8f6aee 增加回调地址设置 2025-05-13 23:48:58 +08:00
d9cf1f12b4 Merge tag '1.1.0' into develop 2025-05-13 22:00:09 +08:00
d37f452946 Merge branch 'release/1.1.0' 2025-05-13 22:00:08 +08:00
ef5eddbc8d 1.1.0 2025-05-13 21:59:57 +08:00
9939ee4c8b 使用标准日志接口 2025-05-13 21:59:28 +08:00
2ff864b8c8 使用workman 2025-05-13 15:49:27 +08:00
b3a5c320c1 Merge branch 'release/1.0.3' 2025-05-11 17:43:57 +08:00
c1547e191c Merge tag '1.0.3' into develop 2025-05-11 17:43:57 +08:00
90f683d655 1.0.3 2025-05-11 17:43:47 +08:00
8b076e52a9 注销状态 2025-05-11 17:43:19 +08:00
8d17cb73ab Merge branch 'release/1.0.2' 2025-05-08 23:46:03 +08:00
2100f37ffa Merge tag '1.0.2' into develop 2025-05-08 23:46:03 +08:00
48720a5912 1.0.2 2025-05-08 23:45:53 +08:00
6e5ec4f056 更新依赖库到 gzzule ^7.0 2025-05-08 23:45:35 +08:00
13 changed files with 642 additions and 28 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ composer.lock
.idea/ .idea/
vendor/ vendor/
tests/prikey.pem tests/prikey.pem
demo.php

View File

@@ -1,11 +1,13 @@
### 安装 ### 安装
```shell ```shell
composer require maiyoule/mqttclient_author composer require maiyoule/mqttclient_author
``` ```
### 配置 ### 配置
```php ```php
$manager = new MQTTManager(); $manager = new AuthorServerClient();
//设置APPID //设置APPID
$manager->setAppId(''); $manager->setAppId('');
//设置私钥 //设置私钥
@@ -14,6 +16,8 @@ $manager->setPrivateKey('');
### 使用 ### 使用
#### 创建
```php ```php
//创建MQTT用户 //创建MQTT用户
$request = new AppUserCreateRequest(); $request = new AppUserCreateRequest();
@@ -33,3 +37,13 @@ $data = $biz->getData();
//.... //....
print_r($data); print_r($data);
``` ```
#### 注销登录
```php
$request=new \cn\com\maiyoule\mqttclient\biz\AppUserLogoutRequest();
$request->setUsername('xxxxx');
$biz=$manager->exec($request);
echo sprintf('注销结果:%b',$biz->isSuccess())
```

View File

@@ -2,7 +2,7 @@
"name": "maiyoule/mqttclient_author", "name": "maiyoule/mqttclient_author",
"type": "library", "type": "library",
"description": "MQTT管理模块操作库", "description": "MQTT管理模块操作库",
"version": "1.0.1", "version": "1.2.1",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
@@ -13,8 +13,10 @@
], ],
"require": { "require": {
"php": ">=8.0", "php": ">=8.0",
"guzzlehttp/guzzle": "~6.0", "guzzlehttp/guzzle": "^7.0",
"phpseclib/phpseclib": "~3.0" "phpseclib/phpseclib": "~3.0",
"workerman/mqtt": "^2.2",
"psr/log": "^3.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.0"

282
src/AuthorAppMQTTClient.php Normal file
View File

@@ -0,0 +1,282 @@
<?php
namespace cn\com\maiyoule\mqttclient;
use cn\com\maiyoule\mqttclient\biz\client\SendMQTTMessage;
use Psr\Log\LoggerInterface;
use Workerman\Mqtt\Client;
use Workerman\Worker;
/**
* MQTT 客户端封装,用于主管理员
*/
class AuthorAppMQTTClient
{
private static ?self $instance = null;
public static function getInstance(): self
{
if (self::$instance == null) {
self::$instance = new self();
}
return self::$instance;
}
private string $appid = '';
private string $server = 'mqtt://mqttauthor.maiyoule.com.cn';
private int $port = 1883;
private string $clientId = '';
private string $username = '';
private string $password = '';
private ?Client $client = null;
public function getServer(): string
{
return $this->server;
}
public function setServer(string $server): AuthorAppMQTTClient
{
$this->server = $server;
return $this;
}
public function getPort(): int
{
return $this->port;
}
public function setPort(int $port): AuthorAppMQTTClient
{
$this->port = $port;
return $this;
}
public function getClientId(): string
{
return $this->clientId;
}
public function setClientId(string $clientId): AuthorAppMQTTClient
{
$this->clientId = $clientId;
return $this;
}
public function getUsername(): string
{
return $this->username;
}
public function setUsername(string $username): AuthorAppMQTTClient
{
$this->username = $username;
return $this;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): AuthorAppMQTTClient
{
$this->password = $password;
return $this;
}
public function getAppid(): string
{
return $this->appid;
}
public function setAppid(string $appid): AuthorAppMQTTClient
{
$this->appid = $appid;
return $this;
}
private LoggerInterface $logger;
public function getLogger(): LoggerInterface
{
return $this->logger;
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @throws \Exception
*/
private function __construct()
{
if (class_exists('Worker')) {
throw new \Exception('没有找到Workman');
}
}
private ?Worker $worker;
/**
* 启动服务
* @return bool
* @throws \Throwable
*/
public function start(): bool
{
$this->worker = new Worker();
$this->worker->onWorkerStart = function () {
$this->logger->info('Worker 启动');
$this->__startWorker();
};
$this->worker->onMessage = function ($conn, $data) {
$this->logger->debug($data);
};
$this->worker->onWorkerStop = function () {
$this->logger->info('Worker 停止');
$this->__stopWorker();
};
$this->worker->onError = function () {
$this->logger->error('Worker启动遇到错误');
};
if (!Worker::isRunning()) {
Worker::runAll();
}
return true;
}
/**
* 停止
* @return void
*/
public function stop(): void
{
if (is_null($this->worker)) {
return;
}
$this->worker->stop();
$this->worker = null;
}
private function __stopWorker(): void
{
if (is_null($this->client)) {
return;
}
$topic = sprintf('biz/%s/#', $this->getAppid());
$this->client->unsubscribe($topic);
//清理订阅
$this->client->disconnect();
}
/**
* @throws \Exception
*/
private function __startWorker(): void
{
//参考 https://www.workerman.net/doc/workerman/components/workerman-mqtt.html
$option = [
'keepalive' => 29,
'client_id' => $this->getClientId(),
'protocol_name' => 'MQTT',
'protocol_level' => 5,
'clean_session' => true,
'reconnect_period' => 2,
'connect_timeout' => 10,
'username' => $this->getUsername(),
'password' => $this->getPassword(),
'resubscribe' => true,
'debug' => false
];
$address = sprintf('mqtt://%s:%d', $this->getServer(), $this->getPort());
$this->logger->debug('建立MQTT连接到' . $address);
$this->client = new Client($address, $option);
//连接回调
$this->client->onConnect = function (Client $connection) {
$this->logger->debug('MQTT 连接成功');
$topic = sprintf('biz/%s/#', $this->getAppid());
$connection->subscribe($topic, ['qos' => 0], function (\Exception|null $exception, array $granted) {
if (!is_null($exception)) {
$this->logger->error($exception->getMessage());
}
$this->logger->debug('主题订阅:' . json_encode($granted));
});
$this->callEvent(self::EVENT_CONNECT, $connection);
};
$this->client->onClose = function () {
$this->logger->debug('MQTT 连接断开');
$this->callEvent(self::EVENT_CLOSE);
};
$this->client->onMessage = function (string $topic, string $data, Client $client) {
//收到消息
$this->logger->debug(sprintf('MQTT 收到消息 Topic:%s Data:%s', $topic, $data));
$this->callEvent(self::EVENT_MESSAGE, $topic, $data, $client);
};
//遇到错误
$this->client->onError = function (\Exception $err) {
$this->logger->error('MQTT 遇到错误' . $err->getMessage());
$this->callEvent(self::EVENT_ERROR, $err);
};
$this->client->connect();
}
const EVENT_CONNECT = 'connect';
const EVENT_CLOSE = 'close';
const EVENT_MESSAGE = 'message';
const EVENT_ERROR = 'error';
private array $mapEvent = [];
public function registerEvent(string $event, \Closure $callback): AuthorAppMQTTClient
{
$this->mapEvent[$event][] = $callback;
return $this;
}
private function callEvent(string $event, ...$args): void
{
if (isset($this->mapEvent[$event])) {
foreach ($this->mapEvent[$event] as $callback) {
call_user_func_array($callback, $args);
}
}
}
/**
* 发送消息到目标客户端
* @param SendMQTTMessage $message
* @return bool
*/
public function sendToTarget(SendMQTTMessage $message): bool
{
//组装主题
$topic = sprintf('biz/%s/%s/%s/downstream', $this->getAppid(), $message->getBiz(), $message->getTargetId());
if (is_null($this->client)) {
return false;
}
$options = [
'qos' => $message->getQoS(),
'retain' => $message->isRetain(),
'dup' => false
];
$this->client->publish($topic, $message->getBiz(), $options);
return true;
}
}

View File

@@ -8,7 +8,10 @@ use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
class MQTTManager /**
* MQTT服务端接口
*/
class AuthorServerClient
{ {
/** /**
* 私钥 * 私钥

View File

@@ -0,0 +1,37 @@
<?php
namespace cn\com\maiyoule\mqttclient\biz;
use cn\com\maiyoule\mqttclient\IRequest;
class AppUpdateCallbackRequest extends IRequest
{
public function path(): string
{
return 'app/callback/update';
}
public function body(): array
{
return [
'url' => $this->getUrl()
];
}
private string $url;
/**
* @return string
*/
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): void
{
$this->url = $url;
}
}

View File

@@ -18,21 +18,59 @@ class AppUserCreateRequest extends IRequest
'password' => $this->getPassword(), 'password' => $this->getPassword(),
'fettle' => $this->getFettle(), 'fettle' => $this->getFettle(),
'role' => $this->getRole(), 'role' => $this->getRole(),
'biz' => join(',', $this->getBiz()) 'publish' => join(',', $this->getPublish()),
'subscribe' => join(',', $this->getSubscribe()),
'expire' => $this->expireAt
]; ];
} }
private array $biz = []; private array $subscribe = [];
public function getBiz(): array public function getSubscribe(): array
{ {
return $this->biz; return $this->subscribe;
} }
public function setBiz(array $biz): void public function setSubscribe(array|string $subscribe): void
{ {
$this->biz = $biz; if (is_string($subscribe)) {
$subscribe = [$subscribe];
} }
$this->subscribe = $subscribe;
}
/**
* @var string 过期时间
*/
private string $expireAt = '';
public function getExpireAt(): string
{
return $this->expireAt;
}
public function setExpireAt(string $expireAt): void
{
$this->expireAt = $expireAt;
}
private array $publish = [];
public function getPublish(): array
{
return $this->publish;
}
public function setPublish(array|string $publish): void
{
if (is_string($publish)) {
$publish = [$publish];
}
$this->publish = $publish;
}
private string $password = ''; private string $password = '';
private string $fettle = ''; private string $fettle = '';

View File

@@ -0,0 +1,36 @@
<?php
namespace cn\com\maiyoule\mqttclient\biz;
use cn\com\maiyoule\mqttclient\IRequest;
class AppUserLogoutRequest extends IRequest
{
public function path(): string
{
return 'mqtt/user/kick';
}
public function body(): array
{
return [
'username' => $this->getUsername()
];
}
private string $username;
public function getUsername(): string
{
return $this->username;
}
public function setUsername(string $username): void
{
$this->username = $username;
}
}

View File

@@ -29,23 +29,37 @@ class AppUserUpdateRequest extends IRequest
if (!is_null($this->fettle)) { if (!is_null($this->fettle)) {
$data['fettle'] = $this->fettle; $data['fettle'] = $this->fettle;
} }
if (!is_null($this->biz)) { if (!empty($this->publish)) {
$data['biz'] = join(',', $this->biz); $data['publish'] = join(',', $this->getPublish());
}
if (!empty($this->subscribe)) {
$data['subscribe'] = join(',', $this->getSubscribe());
} }
return $data; return $data;
} }
private ?array $biz = null; private array $subscribe = [];
public function getBiz(): array public function getSubscribe(): array
{ {
return $this->biz; return $this->subscribe;
} }
public function setBiz(array $biz): void public function setSubscribe(array|string $subscribe): void
{ {
$this->biz = $biz; if (is_string($subscribe)) {
$subscribe = [$subscribe];
}
$this->subscribe = $subscribe;
}
private array $publish = [];
public function getPublish(): array
{
return $this->publish;
} }
private string $username; private string $username;

View File

@@ -0,0 +1,34 @@
<?php
namespace cn\com\maiyoule\mqttclient\biz;
use cn\com\maiyoule\mqttclient\IRequest;
class AppUsersRequest extends IRequest
{
public function path(): string
{
return 'mqtt/users';
}
private string $role;
public function getRole(): string
{
return $this->role;
}
public function setRole(string $role): void
{
$this->role = $role;
}
public function body(): array
{
return [
'role' => $this->role,
];
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace cn\com\maiyoule\mqttclient\biz\client;
class SendMQTTMessage
{
/**
* @var string 业务标识符
*/
private string $biz;
/**
* @var string 目标客户端ID
*/
private string $targetId;
private string $message;
private int $qoS = 0;
private bool $retain = false;
private function __construct()
{
}
public static function create(string $biz, string $targetId, string $message, int $qos = 0, bool $retain = false): SendMQTTMessage
{
$instance = new self();
$instance->setBiz($biz);
$instance->setTargetId($targetId);
$instance->setMessage($message);
$instance->setQoS($qos);
$instance->setRetain($retain);
return $instance;
}
const QOS_0 = 0;
const QOS_1 = 1;
const QOS_2 = 2;
public function isRetain(): bool
{
return $this->retain;
}
public function setRetain(bool $retain): void
{
$this->retain = $retain;
}
public function getQoS(): int
{
return $this->qoS;
}
public function setQoS(int $qoS): void
{
$this->qoS = $qoS;
}
public function getMessage(): string
{
return $this->message;
}
public function setMessage(string $message): void
{
$this->message = $message;
}
public function getTargetId(): string
{
return $this->targetId;
}
public function setTargetId(string $targetId): void
{
$this->targetId = $targetId;
}
public function getBiz(): string
{
return $this->biz;
}
public function setBiz(string $biz): void
{
$this->biz = $biz;
}
}

View File

@@ -2,32 +2,43 @@
namespace cn\com\maiyoule\mqttclient\test; namespace cn\com\maiyoule\mqttclient\test;
use cn\com\maiyoule\mqttclient\biz\AppUpdateCallbackRequest;
use cn\com\maiyoule\mqttclient\biz\AppUserCreateRequest; use cn\com\maiyoule\mqttclient\biz\AppUserCreateRequest;
use cn\com\maiyoule\mqttclient\biz\AppUserDeleteRequest; use cn\com\maiyoule\mqttclient\biz\AppUserDeleteRequest;
use cn\com\maiyoule\mqttclient\biz\AppUserUpdateRequest; use cn\com\maiyoule\mqttclient\biz\AppUserUpdateRequest;
use cn\com\maiyoule\mqttclient\exception\ApiException; use cn\com\maiyoule\mqttclient\exception\ApiException;
use cn\com\maiyoule\mqttclient\MQTTManager; use cn\com\maiyoule\mqttclient\AuthorServerClient;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class AppManagerTest extends TestCase class AppManagerTest extends TestCase
{ {
private MQTTManager $manager; private AuthorServerClient $manager;
protected function setUp(): void protected function setUp(): void
{ {
$this->manager = new MQTTManager(); $this->manager = new AuthorServerClient();
$this->manager->setAppId('6MMVTLW66D'); $this->manager->setAppId('6N6QPV2FYD');
$this->manager->setPrivateKey(file_get_contents(__DIR__ . '/prikey.pem')); $this->manager->setPrivateKey(file_get_contents(__DIR__ . '/prikey.pem'));
$this->manager->setApi('http://localhost:8000/api/'); $this->manager->setApi('https://mqttauthor_dev.maiyoule.com.cn/api/');
$this->manager->setDebug(true); $this->manager->setDebug(true);
} }
public function testAdmin()
{
$request = new AppUserCreateRequest();
$request->setPassword('123');
$request->setRole('admin');
$request->setBiz('ch');
$biz = $this->manager->exec($request);
$this->assertTrue($biz->isSuccess(), $biz->getMessage());
}
public function testRunUser() public function testRunUser()
{ {
try { try {
$request = new AppUserCreateRequest(); $request = new AppUserCreateRequest();
$request->setPassword('111'); $request->setPassword('111');
@@ -71,4 +82,16 @@ class AppManagerTest extends TestCase
} }
} }
public function testUpdateAppUrl()
{
$request = new AppUpdateCallbackRequest();
$request->setUrl('http://202.200.18.46:8000/api.php');
try {
$biz = $this->manager->exec($request);
$this->assertTrue($biz->isSuccess(), $biz->getMessage());
} catch (GuzzleException|ApiException $e) {
$this->fail($e->getMessage());
}
}
} }

View File

@@ -0,0 +1,39 @@
<?php
namespace cn\com\maiyoule\mqttclient\test;
use cn\com\maiyoule\mqttclient\AuthorAppMQTTClient;
use PHPUnit\Framework\TestCase;
class AuthorAppMQTTClientTest extends TestCase
{
private AuthorAppMQTTClient $client;
protected function setUp(): void
{
}
protected function tearDown(): void
{
$this->client->stop();
}
public function testInit()
{
$this->client = AuthorAppMQTTClient::getInstance();
$this->client->setServer('202.200.18.22');
$this->client->setAppid('6N6QPV2FYD');
$this->client
->setClientId('6NEC8SKNK5')
->setUsername('6N6QPV2FYD')
->setPassword('123')
->registerEvent(AuthorAppMQTTClient::EVENT_CONNECT, function ($connection) {
print_r('连接成功');
});
$result = $this->client->start();
self::assertTrue($result, '启动失败');
}
}