短信营销 (php示例)

前段时间实现一个短信营销的需求。

怎么实现这个功能呢?

在处理大数据时 记住必须使用异步处理

 短信营销发送大量数据如何处理

解决办法是

增加预处理操作,就是提前把队列筛选出来,跑出来,到点就直接发送,拆成3步,第一步预处理,第二步10个进程直接发,第三步将 像用户 的发送记录id更新 ,发送内容全部添加进数据库中,方便后续查看错误。(在这个过程中使用3个脚本执行)

将 预处理的数据 存入 redis 中 每次 200条数据进行存储  (存储数量根据短信接口提供商而定 )

发送脚本 执行发送 时间到达就执行推送 ,没有到达重新存入队列

数据库操作能后置就后置,这样可以大大减少 执行的时间

具体代码 

发送短信服务商 大汉三通短信下发(不同内容多个号码) - oss短信下发(不同内容多个号码)-该接口手册编写目的在于描述短信云CTC-OSS(以下简称CTC- OSS)具体的接口协议,供设计和开发人员在开发过程中参考,也使用户对该平台的短信上、下行流程有一个全面的了解。http://help.dahantc.com/docs/oss/1apkb3jmcqlm9.html

用户管理

        短信营销 (php示例)_第1张图片

模版管理

模版示例: 

【XXXX】你好,这是白模板${1,10}示例

其中${1,10}就是一个变量 ,【xxxx】是模版签名

短信营销 (php示例)_第2张图片

人群包管理

生成条件是由管理员自定义,将用户管理中的字段 生成 键值对应的 json 存储在数据库中 在发送时通过 sql 筛选 从而达到用户 筛选的效果 

短信营销 (php示例)_第3张图片

json示例: {"username":"name","gender":"famale","age":["20","50"],"zodiac":["niu","long","she","ma","unknown"],"createtime":"2021-09-29 00:00:00 - 2022-09-29 23:59:59"}

短信营销 (php示例)_第4张图片

发送管理

计划任务参数:是用来在用户点击短链时 区分是那一个短信营销记录

产品原链接:用于生成短链接,在生成短链接之前拼接计划任务参数

短链接:用于发送时,用户点击的链接。

为什么用短链接:

        1. 缩短短信长度,降低成本

        2. 视觉效果更好

        3. 可统计点击数量 (前提条件是使用平台的短链接)

变量参数:则是短信模版中的变量要替换的内容 json 格式 

短信营销 (php示例)_第5张图片

(下拉选项中的 姓名和属相,是在发送时用于区分替换短信模版时直接替换用户表里的数据)

示例:{"1":["\u59d3\u540d",""],"2":["","\u77ed\u4fe1"]}

(参数三是短链接内容,由于是系统生成所以不给管理员添加)

【XXXX】你好${1,10},这是白模板${1,10}示例,链接 xxx.xxx.com/${1,10}

 我们把其中的变量替换成  变量参数则是。

【XXXX】你好张三,这是白模板短信示例 ,链接 xxx.xxx.com/s4dcz(这就是替换后的内容)

限制人数:用于小批量用户的发送

发送记录,支付产品默认值 '-'

短信营销 (php示例)_第6张图片

 正式开始

        像短信营销这种定时任务,那么我们想到的肯定是使用脚本执行,那么数数据肯定是存在redis队列中 脚本不断读取队列,判断执行。

 1、生成对应的短信内容


    /**
     * 生成模版内容
     * @param $Template 模版内容 【XXXX】你好${1,10},这是白模板${1,10}示例,链接 xxx.xxx.com/${1,10}
     * @param $variable 变量参数 {"1":["\u59d3\u540d",""],"2":["","\u77ed\u4fe1"]}
     * @param $short_url 短链接  http://www.xxxx.com/b4a89
     * @return array
     *  返回内容 {
    "str":"你好#姓名1#,这是白模板#自定义2#示例,链接 xxx.xxx.com/b4a89",
    "sing":"【XXXX】",
    "VariableCount":3,
    "variableArray":{
    "1":"姓名",
    "2":"短信"
    },
    "short_url":"b4a89"
    }
     */
    function getTemplateStr($Template,$variable,$short_url){
//        获取短链接路由
        $short_url = preg_replace("/(http):\/\/([^\/]+)/i",'',$short_url);
        $short_url = str_replace('/','',$short_url);
//            获取短信模板 签名 如果不传入签名 使用默认签名
        $sing = preg_match_all("/\[.*?\]|【.*?】/",$Template,$SingArray);
        $sing = $SingArray[0][0];
//        PHP正则匹配统计变量个数
        preg_match_all('/\${(.*?)}/ism',$Template,$VariableArray);

//            记录变量出现的位置
        $index = 0;
//            存储所有变量 出现的位置 和变量长度
        $VariableInfo = [];
//            查询当前变量的位置
        foreach ($VariableArray[0] as $key => $value){
            $StrIndex = mb_strpos($Template,$value,$index);
//                下一次查询最开始的位置
            $index = $StrIndex+strlen($value);

            $VariableInfo[$key]['index'] = $StrIndex;
            $VariableInfo[$key]['lenght'] = strlen($value);
            $VariableInfo[$key]['max'] = substr(explode(',',$value)[1],0,-1);

        }

        $variable = json_decode($variable,true);
//            添加变量的个数
        $count = count($VariableArray[0]);

        $variableArray = [];
        if (!empty($variable) && is_array($variable)){
            foreach ($variable as $key => $value){
                if (!empty($value[0])){
                    $variableArray[$key] = $value[0];
                }elseif (!empty($value[1])){
                    $variableArray[$key] = $value[1];
                }
            }
        }
        $TemplateSubstr = '';
//                截取开始位置
        $start = 0;
//            循环变量
        for ($i = 1; $i <= $count; $i++){
//                临时 存储 模板内容 用于字符截取
            $TemporaryTemplate = $Template;
//                记录模板总长度
            if (isset($variableArray[$i])){
                if($variableArray[$i] == '姓名'){
//                            截取当前变量之前的字符串 截取开始位置是:上次变量出现后最后结束的位置 、 截取的字符串长度:等于当前变量开始位置 - 上个变量出现的最后结束位置
                    $TemplateSubstr .= mb_substr($TemporaryTemplate,$start,$VariableInfo[$i-1]['index']-$start);
                    $start = $VariableInfo[$i-1]['index']+$VariableInfo[$i-1]['lenght'];
//                            拼接替换变量的内容
                    $TemplateSubstr .= "#姓名".$i."#";
//                    $this->output->writeln($TemplateSubstr);
                }elseif ($variableArray[$i] == '属相'){
//                            截取当前变量之前的字符串 截取开始位置是:上次变量出现后最后结束的位置 、 截取的字符串长度:等于当前变量开始位置 - 上个变量出现的最后结束位置
                    $TemplateSubstr .= mb_substr($TemporaryTemplate,$start,$VariableInfo[$i-1]['index']-$start);
                    $start = $VariableInfo[$i-1]['index']+$VariableInfo[$i-1]['lenght'];
//                            拼接替换变量的内容
                    $TemplateSubstr .= "#属相".$i."#";
//                    $this->output->writeln($TemplateSubstr);
                }else{
//                            截取当前变量之前的字符串 截取开始位置是:上次变量出现后最后结束的位置 、 截取的字符串长度:等于当前变量开始位置 - 上个变量出现的最后结束位置
                    $TemplateSubstr .= mb_substr($TemporaryTemplate,$start,$VariableInfo[$i-1]['index']-$start);
                    $start = $VariableInfo[$i-1]['index']+$VariableInfo[$i-1]['lenght'];
//                            拼接替换变量的内容
                    $TemplateSubstr .= "#自定义".$i."#";
//                    $this->output->writeln($TemplateSubstr);
                }
            }else{
//                            截取当前变量之前的字符串 截取开始位置是:上次变量出现后最后结束的位置 、 截取的字符串长度:等于当前变量开始位置 - 上个变量出现的最后结束位置
                $TemplateSubstr .= mb_substr($TemporaryTemplate,$start,$VariableInfo[$i-1]['index']-$start);
                $start = $VariableInfo[$i-1]['index']+$VariableInfo[$i-1]['lenght'];
                $TemplateSubstr .= $short_url;
//                $this->output->writeln($TemplateSubstr);
            }
        }
        $TemplateSubstr .= mb_substr($Template,$start);
        $TemplateSubstr = str_replace($sing,'',$TemplateSubstr);
//        $this->output->writeln($TemplateSubstr);
        return ['str' => $TemplateSubstr,'sing' => $sing,'VariableCount' => $count,'variableArray' => $variableArray,'short_url' => $short_url];
    }

返回内容: 

短信营销 (php示例)_第7张图片

 2、循环总页数(每10w为一页),分页获取所有的用户信息(根据人群包管理中选择的人群包筛选条件生成对应sql)



    /**
     * @param $where 人群包管理生成条件  {"username":"name","gender":"famale","age":["20","50"],"zodiac":["niu","long","she","ma","unknown"],"createtime":"2021-09-29 00:00:00 - 2022-09-29 23:59:59"}
     * @param string $type sql类型 默认是统计总人数  build 查询用户信息sql
     * @param int $page
     * @param int $limit  每次查詢 10w 条
     * @return string
     */
    function BuildSelect($where,$type = 'count',$page = 1,$limit = 100000){
        $where = json_decode($where,true);
        $gender = [
            "famale" => '女',
            'male' => '男',
            'unknown' => 'unknown'
        ];
        $ShengXiao = ['shu'=>'鼠', 'niu'=>'牛', 'hu'=>'虎', 'tu'=>'兔', 'long'=>'龙', 'she'=>'蛇', 'ma'=>'马', 'yang'=>'羊', 'hou'=>'猴', 'ji'=>'鸡', 'gou'=>'狗', 'zhu'=>'猪', 'unknown'=>''];
//    年龄 sql 查询条件
        $AgeSql = '';
        $UsernameSql = '';
        $GenderSql = '';
        $ZodiacSql = '';
        $CreatetimeSql = '';
        foreach ($where as $key => $value){
            switch ($key){
                case 'age':
                    if (!empty($value[0]) && !empty($value[1])){
                        $AgeSql = "( `age` >= '{$value[0]}' AND `age` <= '{$value[1]}' ) ";
                    }
                    break;
                case 'username':
                    if (!empty($value) && $value == 'name'){
                        $UsernameSql = " `username` IS NOT NULL AND `username` != ' '";
                    }elseif (!empty($value) && $value == 'unknown'){
                        $UsernameSql = " ( username is NULL or username = ' ') ";
                    }
                    break;
                case 'gender':
                    if (!empty($value)){
                        $GenderSql = " `gender` = '{$gender[$value]}' ";
                    }
                    break;
                case 'zodiac':
                    foreach ($value as $index => $item){
                        if (!empty($item)){
                            if ($item == 'unknown'){
                                $ZodiacSql .= " zodiac is NULL or zodiac = ' ' or ";
                            }elseif(!empty($item)){
                                $ZodiacSql .= " zodiac = '{$ShengXiao[$item]}' or";
                            }
                        }
                    }
                    $ZodiacSql = substr($ZodiacSql,0,strlen($ZodiacSql)-3);
                    if (!empty($ZodiacSql)){
                        $ZodiacSql = "( {$ZodiacSql} )";
                    }
                    break;
                case 'createtime':
                    if (!empty($value)){
                        $info = explode(' - ',$value);
                        $time1 = strtotime($info[0]);
                        $time2 = strtotime($info[1]);
                        $CreatetimeSql = " (order_create_time >= {$time1} and order_create_time <= {$time2}) ";
                    }
                    break;
            }
        }
        if ($type == 'count'){
            $sql = " SELECT count(id) as `count` FROM `sms_user` WHERE blacklist = 0";
        }elseif($type == 'build'){

            $sql = " SELECT `tel`,`username`,`zodiac` FROM `sms_user` WHERE blacklist = 0";
        }
        if(!empty($AgeSql)){
            $sql .= " AND ".$AgeSql;
        }

        if(!empty($UsernameSql)){
            $sql .= " AND ".$UsernameSql;
        }

        if(!empty($GenderSql)){
            $sql .= " AND ".$GenderSql;
        }

        if(!empty($ZodiacSql)){
            $sql .= " AND ".$ZodiacSql;
        }

        if(!empty($CreatetimeSql)){
            $sql .= " AND ".$CreatetimeSql;
        }
//        是否剔除发送记录
        if ($this->data['send_id_status'] == 1){
            $send_id_array = explode(',',$this->data['send_id']);
            $send_id_where = '';
            foreach ($send_id_array as $key => $value){
                $send_id_where .=' AND `send_id` NOT LIKE \'%-'.$value.'-%\'';
            }
            $sql .= $send_id_where;
        }
//        是否剔除支付产品
        if ($this->data['pay_products_status'] == 1){
            $pay_products_array = explode(',',$this->data['pay_products']);
            $pay_products_where = '';
            foreach ($pay_products_array as $key => $value){
                $pay_products_where .=' AND `pay_products` NOT LIKE \'%-'.$value.'-%\'';
            }
            $sql .= $pay_products_where;
        }
        if($type == 'build'){
            $CurrentPage = $page;
            $CurrentPage == 1 ? $CurrentPage = 0 : $CurrentPage = $CurrentPage - 1;
            $CurrentPages = $CurrentPage * $limit;
            // 判断是否 限制人数
            if ($this->is_astrict){
//                判断 是不是最后一页
                if ($this->CountPage == $page){
//                    最后人数 = 限制人数 - ((总页数 - 1) * 每次查询人数 10w)
                    $limit = $this->last_renshu;
                }
                $sql .= " GROUP BY tel ORDER BY rand() LIMIT {$CurrentPages},{$limit}";
            }else{
                $sql .= " GROUP BY tel LIMIT {$CurrentPages},{$limit}";
            }

        }

        return $sql;
    }

     3、循环用户信息,生成短信内容


    /**
     * @param $Template   模板内容
     * @param $UserInfo   用户数据
     * @param $SendId 发送记录id
     * @return array|bool
     * @param $NowTime  现在时间
     */
    function TemplateContent($Template,$UserInfos,$SendId,$NowTime){
//        判断是否存在模版内容
        if (!empty($Template)){
//            添加变量的个数
            $count = ($Template['VariableCount']);
            $short_url = ($Template['short_url']);

            $variableArray = $Template['variableArray'];
//            存储截取内容并重新拼接发送内容
            $send_limit = 200;
//            存储发送数据
            $data = [];
            $sing = $Template['sing'];
            $userNumber = count($UserInfos);
            $this->output->writeln("用户总数".$userNumber);
            $userForDegree = 0;
            $isum = 0;
//          循环用户
            foreach ($UserInfos as $key => $value){
//                判断用户数据手机号是否是手机号格式
                if(isMobile($value['tel'])){
//                临时 存储 模板内容 用于字符截取
                    $TemplateSubstr = $Template['str'];
//            循环变量 替换内容
                    for ($i = 1; $i <= $count; $i++){
                        if (isset($variableArray[$i])){
                            if($variableArray[$i] == '姓名'){
                                $TemplateSubstr = str_replace("#姓名".$i."#",$value['username'],$TemplateSubstr);
                            }elseif ($variableArray[$i] == '属相'){
                                $TemplateSubstr = str_replace("#属相".$i."#",$value['zodiac'],$TemplateSubstr);
                            }else{
                                $TemplateSubstr = str_replace("#自定义".$i."#",$variableArray[$i],$TemplateSubstr);
                            }
                        }
                    }
                    $msgid = $this->GetRandStr(32);
//                    生成发送数据包   每200为1个队列
                    $data[] = [
                        'msgid' => $msgid,
                        'phones' => $value['tel'],
                        'content' => $TemplateSubstr,
//                        获取报表时携带的参数
                        'params' => ['SmsSendId' => $SendId,'ShortUrl' => $short_url,'NowTime'=>$NowTime],
//                        签名
                        'sign' => $sing,
//                        发送时间
                        'sendtime' => '',
                    ];
//                 当限制人数小于 10 w时 
//                 为什么要有 当前判断 那是因为 在小数据,多任务时,会导致 用户重复发送短信 所以在数据量 小于10万的情况下 立即给发送的用户 添加发送记录,避免用户多次发送,一般大于10万 每天则会只有一条发送任务。那么在大于10万的情况下则不需要 添加用户记录,在提交发送时 将这个步骤后置。和发送日志 一起添加到数据库中。
                    if ($this->count <= 100000){
//                                              给 用户添加发送记录
                        $this->SmsUserModel->query("update sms_user set send_id = CONCAT(`send_id`,'{$SendId}-') where `tel` = '{$value['tel']}'");
                    }
                    $userForDegree++;
//                    判断总数是否 是 200
                    if (count($data) == $send_limit || $userNumber == $userForDegree){
                        $isum++;
                        $this->output->writeln('当前加入队列第'.$isum.'次');
                        $this->output->writeln('当前总数据包个数'.count($data));
                        $datas = [
                            'data' => $data,
                            'send_id' => $SendId,
                            'send_time' => $this->SendTime,
                        ];
                        $data = [];
                        $PlanDate =  json_encode($datas);
// 添加发送数据包到 队列中 执行发送时 读取当前队列
                        $this->redis->rPush('SmsSendCrowdPackage',$PlanDate);
// 由于执行发送时 使用的是多进程 当多任务时 没办法区分当前发送任务是否发送完成 设置缓存 用来判断 发送 提交次数是否和 生成数据包的数量一致 来区分是否完成全部提交
                        $this->redis->incr($SendId.'SmsSubmitCount');
                    }
                }
            }
            unset($UserInfos);
            unset($Template);
            unset($data);
            return true;

        }else{

            return false;

        }


    }



    function GetRandStr($length){
        $str='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $len=strlen($str)-1;
        $randstr='';
        for($i=0;$i<$length;$i++){
            $num=mt_rand(0,$len);
            $randstr .= $str[$num];
        }
        return $randstr;
    }

你可能感兴趣的:(服务器,servlet,php)