消息推送是几乎都会使用上的业务,自己也写过几次质量不同的方案,为以后再遇到能快速解决,列出需求所需,根据需求记录方案。
在公司内写过一个比较完善的消息推送的方案,使用预设定消息模板推送发送任务,通过MQ队列消费任务。但是很快,需求的变动使这套方案已经跟不上了;在一次取快递的时候看到菜鸟驿站的后台网站,他们的推送方式我觉得是可以借鉴的。根据这些我罗列了以下的需求:
- 消息模板功能
- 发送者配置自定义
- 消费者信息配置化
- 同一信息多消费者
- 队列形式持续推送成功为止
- 定时发送
- 接收回调
这次就根据上面的需求完整写出一套用例,通过接入支持短信、邮件发送两个渠道的方式实现这套方案。
1.消息模板功能
该需求主要是预先设置好模板内容,内容里面会包含一些不定变量:比如用户名,时间等,类似输入一个:
你好,{{用户名字}},现在时间是:{{date(Y-m-d)}}
那么解析的时传入变量 {"用户名字":"菜菜酱"} 那么解析后得出结果应该是:
你好,菜菜酱,现在时间是:2022-02-12
考虑到扩展性还支持了注解形式的自定义方法,解决一些比较复杂的情况。以下是主要的完整代码,功能包含验证变量内容是否正确、解析、禁用部分函数和自定义函数调用,使用Hyperf注解功能和一些通用函数,供参考。
<?php
class VariableParse
{
/**
* 判断内容里包含的变量写法是否满足可用
* 可校验括号、单引号、双引号是否闭合、判断是否数字开头的变量
* @param string $content
* @param string $tag 标签以?占位 如果是{{name}}的方式就是{{?}} 如果是[[name]]的方式就是[[?]]
* @return true
* @throws \Exception
*/
public static function validate(string $content, string $tag = '{{?}}'): bool
{
//过长,非主要,见代码文件
}
/**
* 解析内容
* @param string $content 内容
* @param array $variable 变量对象
* @param string $tag 标签以?占位 如果是{{name}}的方式就是{{?}} 如果是[[name]]的方式就是[[?]]
* @return string
* @throws \Exception
*/
public static function parseContent(string $content, array $variable = [], string $tag = '{{?}}')
{
$ruleTag = str_replace('?', '(?<key>.+?)', $tag); //取出最短模板范围正则
$rule = sprintf("/%s/", $ruleTag);
preg_match_all($rule, $content, $match);
foreach ($match['key'] ?? [] as $key) { //遍历模板
$chunkArr = explode('|', $key); //分隔执行块
$chunkArr = array_map('trim', $chunkArr);
$firstKey = array_shift($chunkArr); //判断首个是否变量值
if (is_numeric($firstKey)) { //数字则转结构
if (strpos($firstKey, '.') !== false) {
$value = (float) $firstKey; //浮点数
} else {
$value = (int) $firstKey; //整数
}
} else if (substr($firstKey, 0, 1) == '\'' && substr($firstKey, -1, 1) == '\'') { //普通字符串
$value = substr($firstKey, 1, -1); //字符串
} else if (preg_match('/.+\((.+)?\)/', $firstKey)) { //函数
array_unshift($chunkArr, $firstKey); //写回解析块 让下面的函数解析
if (!isset($value)) {
$value = ""; //定义空值防止无法进入函数处理
}
} else {
$value = data_get($variable, $firstKey ?: null, null); //其他情况
}
if (!is_null($value)) {
foreach ($chunkArr as $chunk) {
//这里需要执行的应该都是函数
preg_match('/(?<function_name>.+)\((?<args>.+)?\)/', $chunk, $match);
$argArr = isset($match['args']) ? explode(',', $match['args']) : [];
$argArr = array_map(function($item) use($value, $variable){
switch ($item) {
case '_逗号':
return ',';
case '$':
return $value;
case 'true':
return true;
case 'false':
return false;
default:
$argRule = "#(?<arg>[$][A-Za-z0-9._$-]+)#";
preg_match_all($argRule, $item, $match);
foreach ($match['arg'] ?? [] as $argKey) {
$argValue = data_get($variable, substr($argKey, 1));
if (is_array($argValue) || is_object($value)) return $argValue;
$item = str_replace($argKey, $argValue, $item);
}
return $item;
}
}, $argArr);
try {
if (in_array($match['function_name'], self::DefinedFunction())) {
$value = self::callDefinedFunction($match['function_name'], $argArr, $value);
} else {
$value = $match['function_name'](...$argArr);
}
} catch (\Throwable $th) {
$value = ''; //防止报错弹出
}
}
}
if (is_array($value) || is_object($value)) {
return $value; //如果是数组或者对象则直接返回
}
$content = str_replace(str_replace('?', $key, $tag), $value ?? '', $content);
}
return $content;
}
/**
* 调用自定义方法
*/
protected static function callDefinedFunction(string $function_name, array $argsArr, $value = null)
{
//过长,非主要,见代码文件
}
/**
* 返回禁止使用的方法
* @return array
*/
protected static function tabooFunction(): array
{
return [
'eval', 'assert', 'call_user_func', 'create_function', 'call_user_func_array', 'system',
'passthru', 'exec', 'pcntl_exec', 'shell_exec', 'popen', 'proc_popen'
];
}
/**
* 返回自定义方法KEy
* @return array
*/
protected static function DefinedFunction(): array
{
//过长,非主要,见代码文件
}
/**
* 验证是否禁止使用的方法
* @throws \Exception
*/
protected static function validateFunction(string $functionName)
{
$functionName = strtolower($functionName);
if (in_array($functionName, self::tabooFunction())) {
throw new \Exception("禁止使用该函数名:" . $functionName);
}
if (!in_array($functionName, self::DefinedFunction()) && !function_exists($functionName)) {
throw new \Exception("函数不存在无法调用:" . $functionName);
}
}
}