[Hyperf]微服务(1) - 消息推送业务解决方案

消息推送是几乎都会使用上的业务,自己也写过几次质量不同的方案,为以后再遇到能快速解决,列出需求所需,根据需求记录方案。

在公司内写过一个比较完善的消息推送的方案,使用预设定消息模板推送发送任务,通过MQ队列消费任务。但是很快,需求的变动使这套方案已经跟不上了;在一次取快递的时候看到菜鸟驿站的后台网站,他们的推送方式我觉得是可以借鉴的。根据这些我罗列了以下的需求:

  1. 消息模板功能
  2. 发送者配置自定义
  3. 消费者信息配置化
  4. 同一信息多消费者
  5. 队列形式持续推送成功为止
  6. 定时发送
  7. 接收回调

这次就根据上面的需求完整写出一套用例,通过接入支持短信、邮件发送两个渠道的方式实现这套方案。

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);
        }
    }
}