nacd为客服排队、IVR、路由管理(FreeSWTICH模块篇)等(五)
nacd为客服排队、客户自有数据库进行注册和DID外呼和接入(三)-号码直接拨入拨出
nacd为客服排队、为线路寻求最佳路由、为目标地址CPS控制流量(二)通过队列,寻找能接通的线路
nacd为客服排队、客户自有数据库进行注册和DID外呼和接入、为线路寻求最佳路由、为目标地址CPS控制流量(一)
以上是前边发出的有关nacd相关的一些介绍,现在我们分享其基于FreeSWITCH的模块,当然,这部分也有OpenSIPS和Kamailio的对应的实现。
理论上nacd在FreeSWTICH中可以替代其管理职能,也就是说路由送进来后,我们其实不需要别的东西了,只要nacd做些控制流转就可以了。以前的排队模块相比之下,就仅仅是一个小的功能模块了。
切记:
-
这个模块,可以直接配置配置文件 ,即可代替以前使用lua、使用curl、使用其它方式和自己的数据库进行对接,即:
<!-- 分机注册相关,db_type : postgres,mysql 两种--><!--//postgresql/*<param name="db_type" value="postgres"/><param name="db_string" value="postgres://postgres:Nway2017@127.0.0.1/cloudcc_web?sslmode=disable"/>*///mysql/*<param name="db_type" value="mysql"/><param name="db_string" value="root:Nway2017@tcp(127.0.0.1:3306)/cloudcc_web"/>*/--><param name="db_type" value="postgres"/><param name="db_string" value="postgres://postgres:Nway2017@127.0.0.1/cloudcc_web?sslmode=disable"/><param name="ext_query_string" value="SELECT extension_pswd FROM nway_extension WHERE extension_number = $1"/><param name="out_did_sql" value="SELECT callout_number,gateway_name FROM nway_extension WHERE extension_type=1 and extension_number = $1"/><!-- 使用did号码外呼时的查询语句,必须要两个返回值--><param name="in_did_sql" value="SELECT extension_number FROM nway_extension WHERE extension_type=1 and callout_number = $1"/><!-- 使用did号码呼入时的查询语句,必须只要一个返回值-->
2. 由于直接使用了动态库规避了FreeSWITCH对数据库的依赖,但是由于加载数据库相关的部分是链上链,所以这个模块不能重载,主要是退不出去。
3. 使用这个模块,进行bridge和originate调用时,请使用nbridge和noriginate,调用方式一样。
下载:
链接: https://pan.baidu.com/s/1LPHOqFnluc9BOGrNVSTQ5A?pwd=rwmb
需要把 mod_nacd.so置于 /usr/local/freeswitch/mod/.
需要把nacd.conf.xml置于 /usr/local/freeswitch/conf/autoload_configs/.
需要把 lib/置于 /opt/nway/下
需要把voices 置于 /opt/nway/下
acd_http_server包含源码及对应的调用等,可以按需进行改动。
以下就acd_http_server主要的代码部分进行说明(golang版):
1. 结构体定义
// 定义请求结构体,匹配 FreeSWITCH 发来的 JSON 数据格式type EventRequest struct {SipNetworkIp string `json:"sip_network_ip"`SipNetworkPort string `json:"sip_network_port"`Timestamp string `json:"timestamp"`Event string `json:"event"`Direction string `json:"direction"`CallerNumber string `json:"caller_number"`CallUUID string `json:"call_uuid"`CalleeNumber string `json:"callee_number"`GroupNumber string `json:"group_number"`MiddleNumber string `json:"middle_number"`Dtmf string `json:"dtmf"`AdditionalData map[string]interface{} `json:"additional_data"`}// 定义响应结构体,符合 HTTPResponse 格式type HTTPResponse struct {UUID string `json:"uuid"`Caller string `json:"caller"`Callee string `json:"callee"`Action string `json:"action"`ActionData map[string]string `json:"action_data"`AfterBridge string `json:"after_bridge"`AfterBridgeData string `json:"after_bridge_data"`DtmfLen int `json:"dtmf_len"`DtmfAudio string `json:"dtmf_audio"`DtmfBadAudio string `json:"dtmf_bad_audio"`UseSurvey bool `json:"use_survey"`BridgeFailRing string `json:"bridge_fail_ring"`WaitForAnswerRing string `json:"wait_for_answer_ring"`TransferRing string `json:"transfer_ring"`SurveyRing []string `json:"survey_ring"`AssociateData map[string]interface{} `json:"associate_data"`}
2. 呼叫流的消息及处理
func httpCallFlowHandler(w http.ResponseWriter, r *http.Request) {var eventRequest EventRequest// 解析请求体中的 JSON 数据// 读取并打印请求体内容bodyBytes, err := io.ReadAll(r.Body)if err != nil {http.Error(w, "Failed to read request body", http.StatusInternalServerError)return}bodyString := string(bodyBytes)fmt.Println("received json as string:", bodyString)// 重置 r.Body 以便后续解析r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))if err := json.NewDecoder(r.Body).Decode(&eventRequest); err != nil {http.Error(w, "Invalid request payload", http.StatusBadRequest)return}defer r.Body.Close()var response HTTPResponse// 创建 HTTPResponse 并填充字段if eventRequest.GroupNumber == "120" {response = HTTPResponse{UUID: eventRequest.CallUUID,Caller: eventRequest.CallerNumber,Callee: eventRequest.CalleeNumber,Action: "bridge",ActionData: map[string]string{"ad_number": "10002","ad_gateway": "","ad_display_number": eventRequest.CallerNumber,"ad_timeout": "30","ad_data": "",},AfterBridge: "playback",AfterBridgeData: "10086", //报工号UseSurvey: true, //开启满意度评价BridgeFailRing: "bridge_fail_ring.wav",WaitForAnswerRing: "wait_for_answer_ring.wav",TransferRing: "transfer_ring.wav",AssociateData: eventRequest.AdditionalData,}} else if eventRequest.GroupNumber == "110" {response = HTTPResponse{UUID: eventRequest.CallUUID,Caller: eventRequest.CallerNumber,Callee: eventRequest.CalleeNumber,Action: "dtmf",DtmfLen: 2,DtmfAudio: "/home/ivr.wav",DtmfBadAudio: "/home/ivr_bad.wav",AssociateData: eventRequest.AdditionalData,}}else if eventRequest.GroupNumber == "119" {response = HTTPResponse{UUID: eventRequest.CallUUID,Caller: eventRequest.CallerNumber,Callee: eventRequest.CalleeNumber,Action: "hangup",ActionData: map[string]string{"ad_rings": "goodbye.wav",},AssociateData: eventRequest.AdditionalData,}}else if eventRequest.GroupNumber == "122" {response = HTTPResponse{UUID: eventRequest.CallUUID,Caller: eventRequest.CallerNumber,Callee: eventRequest.CalleeNumber,Action: "playback",ActionData: map[string]string{"ad_rings": "alice.wav",},AssociateData: eventRequest.AdditionalData,}}// 将响应序列化为 JSONresponseJSON, err := json.Marshal(response)if err != nil {http.Error(w, "Failed to build response JSON", http.StatusInternalServerError)return}//fmt.Println("received:", eventRequest)fmt.Println("sent:", string(responseJSON))// 设置响应头和状态码w.Header().Set("Content-Type", "application/json")w.WriteHeader(http.StatusOK)w.Write(responseJSON)}
3. 事件的处理
func pushEventHandler(w http.ResponseWriter, r *http.Request) {var eventRequest EventRequest// 解析请求体中的 JSON 数据bodyBytes, err := io.ReadAll(r.Body)if err != nil {http.Error(w, "Failed to read request body", http.StatusInternalServerError)return}bodyString := string(bodyBytes)fmt.Println("received json event as string:", bodyString)// 重置 r.Body 以便后续解析r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))defer r.Body.Close()if err := json.NewDecoder(r.Body).Decode(&eventRequest); err != nil {http.Error(w, "Invalid request payload", http.StatusBadRequest)return}if eventRequest.Event == "incoming" ||eventRequest.Event == "callout" {//如果有分同,则置忙,如果是呼入的座席}else if eventRequest.Event == "answered"{}else if eventRequest.Event == "hangup" || eventRequest.Event == "canceled"{}}else if eventRequest.Event == "dtmf" {}fmt.Println("From ip:", r.RemoteAddr)//fmt.Println("Extension Event:", eventRequest)// 响应成功w.WriteHeader(http.StatusOK)}
原来我们种种的实现,其实往往是增加了很多的不一样的工作量,所以从fsgui_cloud来说,我们可以在简单项目中,按照这种方式来实现简单路由+复杂排队+IVR+控制。
当然,以上的只是golang用于演示的代码。其它能提供http服务的都可以对接实现。
如果我们是需要对某些号码做did(Direct Inward Dialling) ,两种方式:
-
直接配置 nacd.conf.xml中指定sql,但需要一条,要在FreeSWITCH或OpenSER的配置中,针对呼入和呼出,指定对应的路由,如:
<extension name="Local_ExtensionNin"><condition field="destination_number" expression="^10002$"><action application="nin_did" data=""/></condition></extension><extension name="Local_ExtensionNout"><condition field="destination_number" expression="^18621(\d+)$"><action application="nout_did" data=""/></condition></extension>
2. 在nacd中,由acd_http_server实现,指定对应的号码,如果是外线,则需要指定网关,不管是内线还是外线,都可以进行报工号及满意度等相关工作。
由mod_nacd导出的api或application在FreeSWITCH中的展现
show modules mod_nacdtype,name,ikey,filenameapi,nacd,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nacd_max,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nacd_transfer,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nacd_version,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,noriginate,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nway_threeway,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nway_uuid_hold,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nway_uuid_transfer,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapi,nway_uuid_unhold,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapplication,nacd,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapplication,nbridge,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapplication,nin_did,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapplication,nout_did,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.soapplication,nwaycallout,mod_nacd,/usr/local/freeswitch/mod/mod_nacd.so14 total.
本章就到这里。
宁卫通信