挪车码微信小程序开发(隐私保护通话)

前言

最近,在公司停车场看到很多车玻璃上都贴着挪车码,微信扫码之后会打开微信小程序或者网页,进而拨打电话呼叫车主挪车。相对于传统纸质版挪车牌,挪车码有以下几大优势:

  • 美观,不占用过多空间。挪车码一般都是直接贴到车窗,和年检、保险标志一样,美观且撕下不留痕迹;
  • 隐私保护车主手机号。部分挪车码具备隐私保护机制,也就是说车主和呼叫方之间通话采用的是虚拟号码,并不会暴露车主手机号。这个机制可以防止4s店销售、保险销售等通过真实手机号骚扰车主的可能;
  • 成本极低。传统挪车牌一般都是几元、十几元甚至几十元,而挪车码成本不到1元,所以对车主和商家来讲成本都大幅降低;
  • 运营空间极大。如果你的小程序或者公众号拥有大量的车主信息,那么流量带来的收益是非常可观的。

经典案例分析

下面分析一个扫码挪车经典案例,它是采用的微信小程序模式,之所以经典,完全是因为它是我自己写的😂。

上图是一张二维码,这是一个空码,空码的意思是它没被人绑定过,扫码之后界面如下:

绑定完成之后界面如下:

这时候车主注册过程已经完成了,下面就是呼叫车主的界面:

呼叫方扫码之后,会显示车主车牌号,点击电话通知,可以直接拨打电话:

可以看到,呼叫方和车主都不是171开头的号码,但是双方手机上显示的是171开头的号码,这就是隐私通话,而且10分钟之后双方再拨打这个号码就不通了,当然失效时间开发者可以根据自己的需要修改。这样彻底实现保护号码隐私。

华为云隐私保护通话

开发之前,首先需要了解下隐私保护通话机制。该机制通俗的来讲就是A和B通话,双方都不暴露真实号码,双方屏显号码为C。笔者结合市面上常见的几大云市场,权衡利弊,最后锁定使用华为云隐私保护通话。

进入华为云官网,在产品---企业应用---云通信,找到隐私保护通话,点击后查看产品介绍。

可以看到,该产品有5种模式,AXB模式、AX模式、X模式等。读者可以自己查看了解,我在这里就不一一介绍了。下面分析下我们扫码挪车的场景。

扫码挪车场景

呼叫方扫码挪车会有两种场景,一种是呼叫方本身就是我们的车主,也就是说呼叫方之前在我们这里注册过,我们知道他的手机号,这样A和B我们都知道手机号,所以在这种场景下我们采用AXB模式;另一种场景是呼叫方不是我们的车主,也就是说他并没有在我们这里注册过,我们不知道他的真实号码,在这种场景下我们采取X模式。

有读者会问,这样我们在呼叫之前还需要去判断是不是我们的用户,岂不是麻烦?其实当你真正的了解这两种模式之后你就不会有这种疑问了,AXB模式一个X号码可以绑定1000组关系,在挪车场景中X模式一个X号码在一定时间内只能是“冻结”状态。号码是有月租的,一个月5块钱,当你的用户量大的时候成本还是很高的,所以需要区分。这一问题后续也会讲到。

虚拟号码申请

既然是隐私通话,不暴露真实号码,那么中间肯定是需要虚拟号码的,虚拟号码需要申请开通,如果是前期测试阶段,建议申请一个AXB模式号码,2个X模式号码。账号注册、创建隐私保护通话应用、号码申请等流程在这里就不赘述了,按步骤来就可以。

开发

微信小程序开发我这里不再赘述,读者如有问题可以随时联系我,在这里主要讲的是对接华为云的开发细节,按上面讲的两个场景区分。

场景一:呼叫方是我们的用户

呼叫方既然是我们的用户,那么我们自然知道他的手机号,果断采取AXB模式。首先看一下AXB模式接口文档:

AXB模式绑定接口

可以看到,文档上写的很清晰,其实就是一个post请求,很简单,只需要注意以下几点:

  • 请求Headers参数要严格按照文档上的来,否则请求失败;
  • callerNum和calleeNum要求是全局号码格式,以”+86开头“;
  • relationNum和areaCode是二选一关系,但建议使用areaCode,华为云接口会自动分配可用的虚拟号码。否则开发者还需要自己筛选空闲的relationNum;
  • recordHintTone和lastMinVoice参数如果非空的话,需要提前上传放音文件。

示例:

/**
     * AXB模式号码绑定
     * @param callerNum 主叫
     * @param calleeNum 被叫
     * @param relationNum 虚拟号码X
     * @return
     */
    public static Map<String,Object> bindAXB(String callerNum,String calleeNum,String relationNum){
        Map<String,Object> resultMap = new HashMap<String, Object>();
        if (StringUtils.isEmpty(callerNum) || StringUtils.isEmpty(calleeNum)) {
            resultMap.put("status", false);
            resultMap.put("errorCode", "40001");
            resultMap.put("msg", "参数错误");
            return resultMap;
        }
        // 封装JOSN请求
        JSONObject json = new JSONObject();
        json.put("relationNum", relationNum); // X号码(关系号码)
        json.put("callerNum", callerNum); // A方真实号码(手机或固话)
        json.put("calleeNum", calleeNum); // B方真实号码(手机或固话)
        /**
         * 选填,各参数要求请参考"AXB模式绑定接口"
         */
         json.put("areaCode", "0531"); //城市码
         json.put("callDirection", 0); //允许呼叫的方向--互相呼叫
         //json.put("duration", SystemContant.CallDuration); //绑定关系保持时间
        // json.put("recordFlag", "false"); //是否通话录音
        // json.put("recordHintTone", "recordHintTone.wav"); //录音提示音
         json.put("maxDuration", SystemContant.MaxDuration); //单次通话最长时间
         json.put("lastMinVoice", "lastMinVoice.wav"); //通话最后一分钟提示音
         json.put("privateSms", "false"); //是否支持短信功能
         JSONObject callerHintTone = new JSONObject();
         callerHintTone.put("callerHintTone", "callerHintTone.wav");
         callerHintTone.put("calleeHintTone", "callerHintTone.wav");
         json.put("preVoice", callerHintTone); //个性化通话前等待音*/
         /*JSONArray preVoiceArr = new JSONArray();
         JSONObject callerHintTone = new JSONObject();
         callerHintTone.put("callerHintTone", "callerHintTone.wav");
         preVoiceArr.add(callerHintTone); //设置A拨打X号码时的通话前等待音
         JSONObject calleeHintTone = new JSONObject();
         calleeHintTone.put("calleeHintTone", "callerHintTone.wav");
         preVoiceArr.add(calleeHintTone); //设置B拨打X号码时的通话前等待音
         json.put("preVoice", preVoiceArr); //个性化通话前等待音*/
        String result = HttpUtil.sendPost(SystemContant.HuaWeiAppKey, SystemContant.HuaWeiAppSecret, SystemContant.HuaWeiHttpServer+SystemContant.VirtualCallInteface, json.toString());
        System.out.println(result);
        JSONObject jsonResult = JSONObject.fromObject(result);
        String resultcode = jsonResult.getString("resultcode");
        if(StringUtils.equals("0", resultcode)){
            resultMap.put("status", true);
            resultMap.put("relationNum", jsonResult.getString("relationNum"));
            resultMap.put("subscriptionId", jsonResult.getString("subscriptionId"));
        }else{
            resultMap.put("status", false);
            resultMap.put("relationNum", "");
        }
        resultMap.put("errorCode", resultcode);
        resultMap.put("msg", jsonResult.getString("resultdesc"));
        //绑定sessionId,唯一标识一组绑定关系
        //subscriptionId
        return resultMap;
    }

 

public static String sendPost(String appKey, String appSecret, String url, String jsonBody) {
        DataOutputStream out = null;
        BufferedReader in = null;
        StringBuffer result = new StringBuffer();
        HttpsURLConnection connection = null;
        InputStream is = null;
        HostnameVerifier hv = new HostnameVerifier() {

            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        try {
            trustAllHttpsCertificates();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        try {
            URL realUrl = new URL(url);
            connection = (HttpsURLConnection) realUrl.openConnection();

            connection.setHostnameVerifier(hv);
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Accept", "application/json");
            connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
            connection.setRequestProperty("Authorization",
                    "WSSE realm=\"SDP\", profile=\"UsernameToken\", type=\"Appkey\"");
            connection.setRequestProperty("X-WSSE", buildWsseHeader(appKey, appSecret));

            logger.info("RequestBody is : " + jsonBody);
            connection.setConnectTimeout(3000);
            connection.connect();
            out = new DataOutputStream(connection.getOutputStream());
            out.writeBytes(jsonBody);
            out.flush();
            out.close();

            int status = connection.getResponseCode();
            if (HTTP_STATUS_OK == status) {
                is = connection.getInputStream();
            } else {
                is = connection.getErrorStream();
            }
            in = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            String line = "";
            while ((line = in.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.info("Send Post request catch exception: " + e.toString());
        }
        finally {
            IOUtils.closeQuietly(out);
            IOUtils.closeQuietly(is);
            IOUtils.closeQuietly(in);
            if (null != connection) {
                IOUtils.close(connection);
            }
        }
        return result.toString();
    }

上面示例只是一个post请求,读者仅可作为参考,如果有问题,随时可以与我沟通。

AXB模式解绑接口

同样,解绑接口也是一个post请求,这里也不再赘述。针对挪车码的开发,这两个接口就足够了。

场景二:呼叫方不是我们的用户

呼叫方不是我们的用户,那我们不知道他的号码,这个时候我采用的是X模式。当然读者也可以研究下其他的模式,有可能也适用,我这边只是针对我自己的需求选择X模式。

同样,首先查看接口文档:

接口有很多,我只用到了其中的呼叫事件通知接口。下面介绍该场景下我程序的逻辑:

首先有一个号码池,这个号码池用于存储空闲X号码,呼叫方点击”电话通知“按钮,这时需要从号码池找出一个空闲号码(要加锁,避免并发情况下出问题),将这个号码返回给呼叫方,同时保存记录到数据库。当呼叫方真正呼叫这个虚拟号码时,华为云会通过呼叫事件通知接口通知开发者服务器对应的接口,传参包括主叫号码、被叫号码等信息。被叫号码即虚拟号码,我们这时就可以通过虚拟号码查找数据库记录,返回真实的被叫号码给华为云,这样呼叫方和被叫方就可以正常通话了。一般挪车电话通话时间并不长,我们可以设置10分钟完成挪车操作,这样过期之后虚拟号码回收到号码池。这里只讲到了主体流程,里面的细节问题读者可以自由处理。下面是开发者服务器需要开发的接口示例,该接口用于接收华为云呼叫事件通知。

/**
     * X模式下接收华为传过来的数据
     * @return
     */
    @RequestMapping(value="/billCallBack",method= RequestMethod.POST)
    @ResponseBody
    public Map<String,Object> billCallBack(@RequestBody XCallBackPo xCallBackPo){
        log.error(JsonUtil.toJSON(xCallBackPo));
        Map<String,Object> resultMap = new HashMap<>();
        // 封装JOSN请求
        String eventType = xCallBackPo.getEventType(); // 通知的事件类型
        CallStatusInfo statusInfo = xCallBackPo.getStatusInfo(); // 呼叫状态事件的信息
        if(statusInfo!=null){
            String timestamp = statusInfo.getTimestamp(); // 呼叫事件发生时隐私保护通话平台的UNIX时间戳
            String callUserData = statusInfo.getUserData(); // 用户附属信息
            String callSessionId = statusInfo.getSessionId(); // 通话链路的标识ID
            String caller = statusInfo.getCaller(); // 主叫号码
            String called = statusInfo.getCalled(); // 被叫号码
            Integer stateCode = statusInfo.getStateCode(); // 通话挂机的原因值
            String stateDesc = statusInfo.getStateDesc(); // 通话挂机的原因值的描述
            String origCalleeNum = statusInfo.getOrigCalleeNum(); // 呼叫的原始被叫号码,即隐私号码(X)
            String displayCallerNum = statusInfo.getDisplayCallerNum(); // 主显号码
            String digitInfo = statusInfo.getDigitInfo(); // 预留字段,用于在放音收号场景中携带收号结果(即用户输入的数字)
            String callSubscriptionId = statusInfo.getSubscriptionId(); // 绑定关系ID
            String callNotifyMode = statusInfo.getNotifyMode(); // 通知模式,仅X模式场景携带
            if (StringUtils.isNotEmpty(callNotifyMode)&&"Block".equalsIgnoreCase(callNotifyMode)) {
                resultMap.put("operation", "connect"); // 用于指示平台的呼叫操作
                // resp.put("operation", "close");
                JSONObject connectInfo = new JSONObject();
                connectInfo.put("displayCalleeNum", called); // 被叫端的来显号码
                //查询真实被叫号码
                String calleeNum = "";
                UserXRecord userXRecord = userCarService.getUserXRecordByRelationNum(called);
                if(userXRecord!=null){
                    if(StringUtils.isEmpty(userXRecord.getCaller())){
                        //第一次通话
                        userXRecord.setCaller(caller);
                        userCarService.updateUserXRecord(userXRecord);
                        //保存openId和电话
//                        userCarService.inserOrUpdateUserCarInfo("",caller,userXRecord.getReserve1(),"","","","");
                    }
                    calleeNum = userXRecord.getCallee();
                    if(StringUtils.equals(calleeNum,caller)){
                        calleeNum = userXRecord.getCaller();
                    }
                }
                if(StringUtils.isEmpty(calleeNum)){
                    messageProducer.sendSystemErrorMessage("", "miniProgram", "UserCarController", "billCallBack", "XCallBackPo:"+xCallBackPo, "{status:false}", "", "X模式下返回华为云被叫号码为空", "saveErrorLog");
                }
                connectInfo.put("calleeNum", calleeNum); // 真实被叫号码
                connectInfo.put("maxDuration", SystemContant.CallDuration/60); // 单次通话最长时间
                connectInfo.put("waitVoice", "callerHintTone.wav"); // 个性化通话前等待音
                connectInfo.put("lastMinVoice", "lastMinVoice.wav"); // 通话最后一分钟提示音
            /*
            connectInfo.put("recordFlag", "true"); // 是否通话录音
            connectInfo.put("recordHintTone", "recordHintTone001.wav"); // 录音提示音
            connectInfo.put("lastMinVoice", "lastMinVoice001.wav"); // 通话最后一分钟提示音
            connectInfo.put("userData", "userflag001");*/ // 用户自定义数据
                resultMap.put("connectInfo", connectInfo); // 指示平台接续被叫通话的参数列表
            /*JSONArray closeInfoArr = new JSONArray();
            JSONObject closeInfo = new JSONObject();
            closeInfo.put("closeHintTone", "closeHintTone001.wav"); // 挂机提示音
            closeInfo.put("userData", "userflag001"); // 用户自定义数据
            closeInfoArr.add(closeInfo);
            resultMap.put("closeInfo", closeInfoArr);*/ // 指示平台结束会话的参数列表
            }
        }
        return resultMap;
    }

以上即为挪车码隐私保护通话相关内容。如有任何问题,欢迎在评论区留言,我会尽快回复~~~

联系作者

 

  • 14
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

男儿何必尽成功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值