前言
最近,在公司停车场看到很多车玻璃上都贴着挪车码,微信扫码之后会打开微信小程序或者网页,进而拨打电话呼叫车主挪车。相对于传统纸质版挪车牌,挪车码有以下几大优势:
- 美观,不占用过多空间。挪车码一般都是直接贴到车窗,和年检、保险标志一样,美观且撕下不留痕迹;
- 隐私保护车主手机号。部分挪车码具备隐私保护机制,也就是说车主和呼叫方之间通话采用的是虚拟号码,并不会暴露车主手机号。这个机制可以防止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;
}
以上即为挪车码隐私保护通话相关内容。如有任何问题,欢迎在评论区留言,我会尽快回复~~~
联系作者