From 9ace85e6040a62bda3201823e36ed8382689733e Mon Sep 17 00:00:00 2001 From: jerry Date: Sat, 20 Feb 2021 16:57:19 +0800 Subject: [PATCH] update to 2.2.5 version again add transaction for SpinLock.php add getAllCQ() for CQ.php fix CQ bug update docs --- composer.json | 5 +- docs/advanced/example/admin.md | 231 ++++++++++++++++++++++++++++++++ docs/component/cqcode.md | 69 +++++++++- docs/event/middleware.md | 2 +- docs/event/robot-annotations.md | 15 +++ docs/index.md | 5 + mkdocs.yml | 6 +- src/ZM/API/CQ.php | 176 ++++++++++++++++++------ src/ZM/Store/Lock/SpinLock.php | 6 + 9 files changed, 462 insertions(+), 53 deletions(-) create mode 100644 docs/advanced/example/admin.md diff --git a/composer.json b/composer.json index 8808cd16..de4e51f9 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ ] }, "require-dev": { - "swoole/ide-helper": "@dev" + "swoole/ide-helper": "@dev", + "phpunit/phpunit": "^9.5" } -} \ No newline at end of file +} diff --git a/docs/advanced/example/admin.md b/docs/advanced/example/admin.md new file mode 100644 index 00000000..a2eaf2e0 --- /dev/null +++ b/docs/advanced/example/admin.md @@ -0,0 +1,231 @@ +# 编写管理员专属功能 + +众所周知,如果大家使用炸毛框架来开发聊天机器人的话,会比较方便。但是有些地方你一定会感觉还是欠缺了点,比如下面这样,你想编写一个只能由机器人管理员,也就是你自己,才能触发的功能: + +```php +/** + * @CQCommand(match="禁言",message_type="group") + */ +public function banSomeone() { + $r1 = ctx()->getNextArg("请输入禁言的人或at他"); + $r2 = ctx()->getFullArg("请输入禁言的时间(秒)"); + $cq = CQ::getCQ($r1); + if ($cq !== null) { + if ($cq["type"] != "at") return "请at或者输入正确的QQ号!"; + $r1 = $cq["params"]["qq"]; + } + // 群内禁言用户 + ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2); + return "禁言成功!"; +} +``` + +这时候,如果只是自己有绝对的权利,可以将自己的 QQ 号写死在注解 `@CQCommand` 中,并限定 `user_id`(假设我的 QQ 号码为 123456): + +```php +/** + * @CQCommand(match="禁言",message_type="group",user_id=123456) + */ +``` + +但是,随着时间的推移,你的机器人伙伴群可能越来越大,这个命令可能不止需要绝对的你来使用,你还要将机器人的部分权利下发给更多的伙伴,怎么办呢?注解里面只能写死的。 + +答案很简单,这时候我们就需要用到框架提供的中间件(Middleware)。中间件说白了就是在事件执行前、后、过程中抛出的异常对其进行阻断和插入代码,比如我们上方在触发禁言这个注解事件前首先要判断执行这个命令的是不是钦定的管理员。 + +## 第一步:定义中间件 + +首先,我们需要定义一个中间件。在框架默认提供的脚手架中,包含了一个叫 `TimerMiddleware.php` 的示例中间件,这个示例中间件的目的是非常简单的,就是判断这个注解事件运行了多长时间。假设你有一个机器人功能,这个功能下的代码需要执行很长时间,可以使用这一注解轻松将事件执行的时间打印到终端上。 + +关于中间件的有关说明,见 [中间件](/event/middleware)。 + +下面我们假设你已经阅读过中间件注解的文档了,我们着手编写一个判断指令执行者是否是指定的管理员 QQ 的中间件。为了省事和让大家方便地复现,我先在脚手架下的目录 `src/Module/Middleware/` 下新建 PHP 类文件 `AdminMiddleware.php`(和 `TimerMiddleware.php` 在同一个目录)。 + +```php +getUserId(); // 从上下文获取发消息的用户 QQ + $admin_list = LightCache::get("admin_list") ?? []; // 从轻量缓存获取管理员列表 + return in_array($r, $admin_list); // 返回这个 QQ 是否在管理员列表中 + } +} +``` + +其中,`@MiddlewareClass("admin")` 的意思是,定义这个类为名字叫 `admin` 的中间件,同时,所有中间件的类**必须**带上 `implements MiddlewareInterface`,统一接口形式。 + +`@HandleBefore()` 代表的是,这个类下的这个函数(onBefore)被标注为这个中间件的 `onBefore` 事件,也就是说,如果有别的注解事件插入了这个 `admin` 中间件,那么执行对应注解事件前都要执行一下 `@HandleBefore` 所绑定的这个函数。而这个绑定的函数只能返回 `bool` 类型的值哦! + +## 第二步:使用中间件 + +使用中间件很简单,在需要阻断的注解事件绑定的函数上再加一个注解就好了!我们以上方的禁言例子说明: + +```php +/** + * @Middleware("admin") + * @CQCommand(match="禁言",message_type="group") + */ +``` + + +^ 假设我是管理员 +) 禁言 1234567 600 +( 禁言成功! +^ 假设我不在管理员名单里 +) 禁言 1234567 900 +^ 机器人没有回复,因为中间件返回了 false,不继续执行 + + +而这时候有朋友又要问了,如果我有一系列管理员命令,假设都在一个叫 `AdminFunc.php` 的模块类里,我是不是还得一个一个地给注解事件写 `@Middleware("admin")` 呢?当然不需要!如果你这个类所有的注解事件都是机器人的聊天事件(`@CQCommand`,`@CQMessage`)的话,可以直接给类注解这个中间件,效果等同于给每一个函数写一次中间件注解。 + +```php +getNextArg("请输入禁言的人或at他"); + $r2 = ctx()->getFullArg("请输入禁言的时间(秒)"); + $cq = CQ::getCQ($r1); + if ($cq !== null) { + if ($cq["type"] != "at") return "请at或者输入正确的QQ号!"; + $r1 = $cq["params"]["qq"]; + } + // 群内禁言用户 + ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2); + return "禁言成功!"; + } + + /** + * @CQCommand(match="解除禁言",message_type="group") + */ + public function unbanSomeone() { + $r1 = ctx()->getNextArg("请输入禁言的人或at他"); + $cq = CQ::getCQ($r1); + if ($cq !== null) { + if ($cq["type"] != "at") return "请at或者输入正确的QQ号!"; + $r1 = $cq["params"]["qq"]; + } + // 群内禁言用户 + ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, 0); + return "解除禁言成功!"; + } + } + ``` + +=== "src/Module/Example/AdminManager.php" + + ```php + getNextArg("请输入要添加管理员的QQ(qq号码,不可at)"); + SpinLock::lock("admin_list"); //如果是多进程模式的话需要加锁 + $ls = LightCache::get("admin_list"); + if (!in_array($qq, $ls)) $ls[] = $qq; + LightCache::set("admin_list", $ls, -2); + SpinLock::unlock("admin_list"); //如果是多进程模式的话需要加锁 + return "成功添加 $qq 到管理员列表!"; + } + } + ``` + + +^ 现在我是 123456 +) 禁言 13579 60 +( 禁言成功! +) 解除禁言 13579 +( 解除禁言成功! +) 添加管理员 98765 +( 成功添加 98765 到管理员列表! +^ 现在我是98765 +) 禁言 13579 +( 请输入禁言的时间(秒) +) 120 +( 禁言成功! + + diff --git a/docs/component/cqcode.md b/docs/component/cqcode.md index 6a898826..1816a910 100644 --- a/docs/component/cqcode.md +++ b/docs/component/cqcode.md @@ -82,15 +82,20 @@ class Hello { CQ 码字符反转义。 +定义:`CQ::encode($msg, $is_content = false)` + +当 `$is_content` 为 true 时,会将 `,` 转义为 `,`。 + | 反转义前 | 反转义后 | | -------- | -------- | | `&` | `&` | | `[` | `[` | | `]` | `]` | +| `,` | `,` | ```php -$str = CQ::decode("[我只是一条普通的文本]"); -// 转换为 "[我只是一条普通的文本]" +$str = CQ::decode("[CQ:at,qq=我只是一条普通的文本]"); +// 转换为 "[CQ:at,qq=我只是一条普通的文本]" ``` ### CQ::encode() @@ -102,6 +107,14 @@ $str = CQ::encode("[CQ:我只是一条普通的文本]"); // $str: "[CQ:我只是一条普通的文本]" ``` +定义:`CQ::encode($msg, $is_content = false)` + +当 `$is_content` 为 true 时,会将 `,` 转义为 `,`。 + +### CQ::escape() + +同 `CQ::encode()`。 + ### CQ::removeCQ() 去除字符串中所有的 CQ 码。 @@ -113,7 +126,7 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]" ### CQ::getCQ() -解析CQ码。 +解析 CQ 码。 - 参数:`getCQ($msg);`:要解析出 CQ 码的消息。 - 返回:`数组 | null`,见下表 @@ -125,6 +138,34 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]" | start | 此 CQ 码在字符串中的起始位置 | | end | 此 CQ 码在字符串中的结束位置 | +### CQ::getAllCQ() + +解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。 + +```php +CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]"); +/* +[ + [ + "type" => "at", + "params" => [ + "qq" => "123", + ], + "start" => 0, + "end" => 13, + ], + [ + "type" => "at", + "params" => [ + "qq" => "456", + ], + "start" => 17, + "end" => 30, + ], +] +*/ +``` + ## CQ 码列表 ### CQ::face() - 发送 QQ 表情 @@ -463,11 +504,31 @@ public function xmlTest() { 发送 QQ 兼容的 JSON 多媒体消息。 -定义:`CQ::json($data)` +定义:`CQ::json($data, $resid = 0)` 参数同上,内含 JSON 字符串即可。 +其中 `$resid` 是面向 go-cqhttp 扩展的参数,默认不填为 0,走小程序通道,填了走富文本通道发送。 + !!! tip "提示" 因为某些众所周知的原因,XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。 +### CQ::_custom() - 扩展自定义 CQ 码 + +用于兼容各类含有被支持的扩展 CQ 码,比如 go-cqhttp 的 `[CQ:gift]` 礼物类型。 + +定义:`CQ::_custom(string $type_name, array $params)` + +| 参数名 | 说明 | +| ----------- | --------------------------------------------------- | +| `type_name` | CQ 码类型,如 `music`,`at` | +| `params` | 发送的 CQ 码中的参数数组,例如 `["qq" => "123456"]` | + +下面是一个例子: + +```php +CQ::_custom("at",["qq" => "123456","qwe" => "asd"]); +// 返回:[CQ:at,qq=123456,qwe=asd] +``` + diff --git a/docs/event/middleware.md b/docs/event/middleware.md index 653210d4..ec9a4790 100644 --- a/docs/event/middleware.md +++ b/docs/event/middleware.md @@ -163,5 +163,5 @@ public function onThrowing(?Exception $e) { 这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。 -`context()` 为获取当前协程空间绑定的 `request` 和 `response` 对象。 +`ctx()` 为获取当前协程空间绑定的 `request` 和 `response` 对象。 diff --git a/docs/event/robot-annotations.md b/docs/event/robot-annotations.md index 2c0f2dc0..a34c03f1 100644 --- a/docs/event/robot-annotations.md +++ b/docs/event/robot-annotations.md @@ -11,10 +11,25 @@ QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理 事件是用户需要从 OneBot 被动接收的数据,有以下几个大类: - [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand),`@CQMessage` 注解处理。 + - [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。 + - [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。 + - [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。 +## 注解事件参照表 + +| 注解名称 | 类所在命名全称 | 作用 | +| ------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------ | +| [`@CQBefore`](/event/robot-annotations/#cqbefore) | `\ZM\Annotation\CQBefore` | OneBot 各类事件前触发的,可当作事件过滤器使用 | +| [`@CQAfter`](/event/robot-annotations/#cqafter) | `\ZM\Annotation\CQAfter` | OneBot 各类事件后触发的 | +| [`@CQMessage`](/event/robot-annotations/#cqmessage) | `\ZM\Annotation\CQMessage` | OneBot 中消息类事件的触发(机器人消息)事件 | +| [`@CQCommand`](/event/robot-annotations/#cqcommand) | `\ZM\Annotation\CQCommand` | OneBot 中消息类事件的触发(机器人消息)事件,但是被封装为指令型的,无需自己切割命令式 | +| [`@CQNotice`](/event/robot-annotations/#cqnotice) | `\ZM\Annotation\CQNotice` | OneBot 中通知类事件的触发(机器人消息)事件 | +| [`@CQRequest`](/event/robot-annotations/#cqrequest) | `\ZM\Annotation\CQRequest` | OneBot 中请求类事件的触发(机器人消息)事件,一般带有请求信息,可联动相关响应的 API 完成功能编写 | +| [`@CQMetaEvent`](/event/robot-annotations/#cqmetaevent) | `\ZM\Annotation\CQMetaEvent` | OneBot 中涉及 OneBot 实现本身的一些和机器人事件无关的元事件,比如 WS 连接的心跳包 | + ## CQMessage() QQ 收到消息后触发的事件对应注解。 diff --git a/docs/index.md b/docs/index.md index 87df4542..a4fc5c74 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,11 @@ > 如果是从 v1.x 版本升级到 v2.x,[点我看升级指南](/advanced/to-v2/)。 +!!! tip "提示" + + 编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request,以此来贡献此文档! + + 炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。 框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。 diff --git a/mkdocs.yml b/mkdocs.yml index 41f533f1..1d4a0f03 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,8 +10,8 @@ theme: favicon: assets/favicon.png language: zh palette: - primary: blue - accent: blue + primary: red + accent: red features: - navigation.tabs extra_javascript: @@ -95,6 +95,8 @@ nav: - 内部类文件手册: advanced/inside-class.md - 接入 WebSocket 客户端: advanced/connect-ws-client.md - 框架多进程: advanced/multi-process.md + - 开发实战教程: + - 编写管理员才能触发的功能: advanced/example/admin.md - FAQ: FAQ.md - 更新日志: - 更新日志(v2): update/v2.md diff --git a/src/ZM/API/CQ.php b/src/ZM/API/CQ.php index fa7e45e4..d6f7bf8d 100644 --- a/src/ZM/API/CQ.php +++ b/src/ZM/API/CQ.php @@ -45,11 +45,11 @@ class CQ */ public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) { return - "[CQ:image,file=" . $file . + "[CQ:image,file=" . self::encode($file, true) . (!$cache ? ",cache=0" : "") . ($flash ? ",type=flash" : "") . (!$proxy ? ",proxy=false" : "") . - ($timeout != -1 ? (",timeout=" . $timeout) : "") . + ($timeout != -1 ? (",timeout=" . intval($timeout)) : "") . "]"; } @@ -64,11 +64,11 @@ class CQ */ public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) { return - "[CQ:record,file=" . $file . + "[CQ:record,file=" . self::encode($file, true) . (!$cache ? ",cache=0" : "") . ($magic ? ",magic=1" : "") . (!$proxy ? ",proxy=false" : "") . - ($timeout != -1 ? (",timeout=" . $timeout) : "") . + ($timeout != -1 ? (",timeout=" . intval($timeout)) : "") . "]"; } @@ -82,10 +82,10 @@ class CQ */ public static function video($file, $cache = true, $proxy = true, $timeout = -1) { return - "[CQ:video,file=" . $file . + "[CQ:video,file=" . self::encode($file, true) . (!$cache ? ",cache=0" : "") . (!$proxy ? ",proxy=false" : "") . - ($timeout != -1 ? (",timeout=" . $timeout) : "") . + ($timeout != -1 ? (",timeout=" . intval($timeout)) : "") . "]"; } @@ -121,7 +121,7 @@ class CQ * @return string */ public static function poke($type, $id, $name = "") { - return "[CQ:poke,type=$type,id=$id" . ($name != "" ? ",name=$name" : "") . "]"; + return "[CQ:poke,type=$type,id=$id" . ($name != "" ? (",name=".self::encode($name, true)) : "") . "]"; } /** @@ -143,10 +143,10 @@ class CQ */ public static function share($url, $title, $content = null, $image = null) { if ($content === null) $c = ""; - else $c = ",content=" . $content; + else $c = ",content=" . self::encode($content, true); if ($image === null) $i = ""; - else $i = ",image=" . $image; - return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]"; + else $i = ",image=" . self::encode($image, true); + return "[CQ:share,url=" . self::encode($url, true) . ",title=" . self::encode($title, true) . $c . $i . "]"; } /** @@ -159,8 +159,21 @@ class CQ return "[CQ:contact,type=$type,id=$id]"; } + /** + * 发送位置 + * @param $lat + * @param $lon + * @param string $title + * @param string $content + * @return string + */ public static function location($lat, $lon, $title = "", $content = "") { - + return "[CQ:location" . + ",lat=".self::encode($lat, true) . + ",lon=".self::encode($lon, true). + ($title != "" ? (",title=".self::encode($title, true)) : "") . + ($content != "" ? (",content=".self::encode($content, true)) : "") . + "]"; } /** @@ -193,10 +206,13 @@ class CQ return " "; } if ($content === null) $c = ""; - else $c = ",content=" . $content; + else $c = ",content=" . self::encode($content, true); if ($image === null) $i = ""; - else $i = ",image=" . $image; - return "[CQ:music,type=custom,url=" . $id_or_url . ",audio=" . $audio . ",title=" . $title . $c . $i . "]"; + else $i = ",image=" . self::encode($image, true); + return "[CQ:music,type=custom,url=" . + self::encode($id_or_url, true) . + ",audio=" . self::encode($audio, true) . ",title=" . self::encode($title, true) . $c . $i . + "]"; default: Console::warning("传入的music type($type)错误!"); return " "; @@ -208,19 +224,36 @@ class CQ } public static function node($user_id, $nickname, $content) { - return "[CQ:node,user_id=$user_id,nickname=$nickname,content=" . self::escape($content) . "]"; + return "[CQ:node,user_id=$user_id,nickname=".self::encode($nickname, true).",content=" . self::encode($content, true) . "]"; + } + + public static function xml($data) { + return "[CQ:xml,data=" . self::encode($data, true) . "]"; + } + + public static function json($data, $resid = 0) { + return "[CQ:json,data=" . self::encode($data, true) . ",resid=" . intval($resid) . "]"; + } + + public static function _custom(string $type_name, $params) { + $code = "[CQ:" . $type_name; + foreach ($params as $k => $v) { + $code .= "," . $k . "=" . self::escape($v, true); + } + $code .= "]"; + return $code; } /** * 反转义字符串中的CQ码敏感符号 - * @param $str + * @param $msg + * @param bool $is_content * @return mixed */ - public static function decode($str) { - $str = str_replace("&", "&", $str); - $str = str_replace("[", "[", $str); - $str = str_replace("]", "]", $str); - return $str; + public static function decode($msg, $is_content = false) { + $msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg); + if ($is_content) $msg = str_replace(",", ",", $msg); + return $msg; } public static function replace($str) { @@ -230,42 +263,97 @@ class CQ } /** - * 转义CQ码 + * 转义CQ码的特殊字符,同encode * @param $msg + * @param bool $is_content * @return mixed */ - public static function escape($msg) { - $msg = str_replace("&", "&", $msg); - $msg = str_replace("[", "[", $msg); - $msg = str_replace("]", "]", $msg); + public static function escape($msg, $is_content = false) { + $msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg); + if ($is_content) $msg = str_replace(",", ",", $msg); return $msg; } - public static function encode($str) { - return self::escape($str); + /** + * 转义CQ码的特殊字符 + * @param $msg + * @param false $is_content + * @return mixed + */ + public static function encode($msg, $is_content = false) { + $msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg); + if ($is_content) $msg = str_replace(",", ",", $msg); + return $msg; } + /** + * 移除消息中所有的CQ码并返回移除CQ码后的消息 + * @param $msg + * @return string + */ public static function removeCQ($msg) { - while (($cq = self::getCQ($msg)) !== null) { - $msg = str_replace(mb_substr($msg, $cq["start"], $cq["end"] - $cq["start"] + 1), "", $msg); + $final = ""; + $last_end = 0; + foreach(self::getAllCQ($msg) as $k => $v) { + $final .= mb_substr($msg, $last_end, $v["start"] - $last_end); + $last_end = $v["end"] + 1; } - return $msg; + $final .= mb_substr($msg, $last_end); + return $final; } + /** + * 获取消息中第一个CQ码 + * @param $msg + * @return array|null + */ public static function getCQ($msg) { - if (($start = mb_strpos($msg, '[')) === false) return null; - if (($end = mb_strpos($msg, ']')) === false) return null; - $msg = mb_substr($msg, $start + 1, $end - $start - 1); - if (mb_substr($msg, 0, 3) != "CQ:") return null; - $msg = mb_substr($msg, 3); - $msg2 = explode(",", $msg); - $type = array_shift($msg2); - $array = []; - foreach ($msg2 as $k => $v) { - $ss = explode("=", $v); - $sk = array_shift($ss); - $array[$sk] = implode("=", $ss); + if (($head = mb_strpos($msg, "[CQ:")) !== false) { + $key_offset = mb_substr($msg, $head); + $close = mb_strpos($key_offset, "]"); + if ($close === false) return null; + $content = mb_substr($msg, $head + 4, $close + $head - mb_strlen($msg)); + $exp = explode(",", $content); + $cq["type"] = array_shift($exp); + foreach ($exp as $k => $v) { + $ss = explode("=", $v); + $sk = array_shift($ss); + $cq["params"][$sk] = self::decode(implode("=", $ss), true); + } + $cq["start"] = $head; + $cq["end"] = $close + $head; + return $cq; + } else { + return null; } - return ["type" => $type, "params" => $array, "start" => $start, "end" => $end]; + } + + /** + * 获取消息中所有的CQ码 + * @param $msg + * @return array + */ + public static function getAllCQ($msg) { + $cqs = []; + $offset = 0; + while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) { + $key_offset = mb_substr($submsg, $head); + $tmpmsg = mb_strpos($key_offset, "]"); + if ($tmpmsg === false) break; // 没闭合,不算CQ码 + $content = mb_substr($submsg, $head + 4, $tmpmsg + $head - mb_strlen($submsg)); + $exp = explode(",", $content); + $cq = []; + $cq["type"] = array_shift($exp); + foreach ($exp as $k => $v) { + $ss = explode("=", $v); + $sk = array_shift($ss); + $cq["params"][$sk] = self::decode(implode("=", $ss), true); + } + $cq["start"] = $offset + $head; + $cq["end"] = $offset + $tmpmsg + $head; + $offset += $tmpmsg + 1; + $cqs[] = $cq; + } + return $cqs; } } diff --git a/src/ZM/Store/Lock/SpinLock.php b/src/ZM/Store/Lock/SpinLock.php index b6ccbdf0..194e120a 100644 --- a/src/ZM/Store/Lock/SpinLock.php +++ b/src/ZM/Store/Lock/SpinLock.php @@ -39,4 +39,10 @@ class SpinLock public static function unlock(string $key) { return self::$kv_lock->set($key, ['lock_num' => 0]); } + + public static function transaction(string $key, callable $function) { + SpinLock::lock($key); + $function(); + SpinLock::unlock($key); + } }