基于freeswitch1.6的IVR智能语音机器人交互逻辑lua脚本
生活随笔
收集整理的這篇文章主要介紹了
基于freeswitch1.6的IVR智能语音机器人交互逻辑lua脚本
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
package.path = "/usr/local/share/lua/5.2/?.lua"
package.cpath = "/usr/local/lib/lua/5.2/?.so;"
local socket = require "socket"
local http = require "socket.http"
http.TIMEOUT = 10
local ltn12 = require("ltn12")--智能客服機器人的hashcode
local robothashcode = ""
--能力平臺appkey
local appKey = "ac5d5453"
--機器人問答渠道id
local channelId = "1";
local receiverId = 8185
--broker問答地址
local brokerAddress = "http://10.248.17.3:8080/CSRBroker"
--主叫號碼
local call_num_link = session:getVariable("caller_id_number")
--被叫號碼
local talkerId = session:getVariable("destination_number")local uuid = argv[1]math.randomseed(os.time())
--通話的唯一標識
local callUserId = math.random() * 100000000000000
--TTS名稱
local ttsName = "ZhuRuiv9"--用戶信息接口
local getCustomerInfo = "http://10.248.17.3:8083/api/customer/basic/getCustomerInfo"--結果回傳接口
local updateUserInfoUrl = "http://10.248.17.3:8083/api/customer/basic/updateResult"--呼叫開始時間
local startTime = ""
local endTime = ""
--local record_file = session:getVariable("record_file")--錄音文件保存地址
local subdir = os.date("%Y-%m-%d", os.time())
local datestr = session:getVariable("datestr")
local record_file = "/home/data/recordings/archive/renrendai/" .. subdir .. "/" .. call_num_link .. "_" .. datestr .. ".wav"
function printLog(text)if (text) thenfreeswitch.consoleLog("warning", text .. "\n")elsefreeswitch.consoleLog("warning", "object is nil")end
end--字符串分隔方法
function string_split(str, split_char)local sub_str_tab = {};while (true) dolocal pos = string.find(str, split_char);if (not pos) thensub_str_tab[#sub_str_tab + 1] = str;break;endlocal sub_str = string.sub(str, 1, pos - 1);sub_str_tab[#sub_str_tab + 1] = sub_str;str = string.sub(str, pos + 1, #str);endreturn sub_str_tab;
end--分隔主叫號碼傳遞參數
local str_Arr = string_split(call_num_link, "*")
--主叫號碼
--local call_num = "15034032390"
local call_num = str_Arr[1]
--業務類型
--local transparam = "7"
local transparam = str_Arr[2]
--local userId = str_Arr[3]
local userId = "20"--[[
--主叫號碼
local call_num = "15034032390"
--業務類型
local transparam = 7
local userId = "20"--分隔主叫號碼傳遞參數
local str_Arr = string_split(call_num_link, "*")
if(#str_Arr == 3) thencall_num = str_Arr[1]transparam = str_Arr[2]userId = str_Arr[3]
end
--]]freeswitch.consoleLog("warning", "*******************LOG*****************" .. uuid)
freeswitch.consoleLog("warning", "*****************call_num_link:*****************[" .. call_num_link .. "]")
freeswitch.consoleLog("warning", "*****************talkerId:*****************[" .. talkerId .. "]")callUserId = "renrendai_" .. call_num .. "_" .. math.floor(callUserId)function parseargs_xml(s)local arg = {}string.gsub(s, "(%w+)=([\"'])(.-)%2", function(w, _, a)arg[w] = aend)return arg
endfunction parse_xml(s)local stack = {};local top = {};table.insert(stack, top);local ni, c, label, xarg, empty;local i, j = 1, 1;while true doni, j, c, label, xarg, empty = string.find(s, "<(%/?)(%w+)(.-)(%/?)>", i);if not ni thenbreakendlocal text = string.sub(s, i, ni - 1);if not string.find(text, "^%s*$") thentable.insert(top, text);endif empty == "/" thentable.insert(top, { label = label, xarg = parseargs_xml(xarg), empty = 1 });elseif c == "" thentop = { label = label, xarg = parseargs_xml(xarg) };table.insert(stack, top);elselocal toclose = table.remove(stack);top = stack[#stack];if #stack < 1 thenerror("nothing to close with " .. label);endif toclose.label ~= label thenerror("trying to close " .. toclose.label .. " with " .. label);endtable.insert(top, toclose);endi = j + 1;endlocal text = string.sub(s, i);if not string.find(text, "^%s*$") thentable.insert(stack[stack.n], text);endif #stack > 1 thenerror("unclosed " .. stack[stack.n].label);endreturn stack[1];
end-- Used to parse the XML results.
function getResults(s)local xml = parse_xml(s);local stack = {}local top = {}table.insert(stack, top)top = { grammar = xml[1].xarg.grammar, score = xml[1].xarg.score, text = xml[1][1][1] }table.insert(stack, top)return top;
endlocal function json2true(str, from, to)return true, from + 3
endlocal function json2false(str, from, to)return false, from + 4
endlocal function json2null(str, from, to)return nil, from + 3
endlocal function json2nan(str, from, to)return nul, from + 2
endlocal numberchars = {['-'] = true,['+'] = true,['.'] = true,['0'] = true,['1'] = true,['2'] = true,['3'] = true,['4'] = true,['5'] = true,['6'] = true,['7'] = true,['8'] = true,['9'] = true,
}local function json2number(str, from, to)local i = from + 1while (i <= to) dolocal char = string.sub(str, i, i)if not numberchars[char] thenbreakendi = i + 1endlocal num = tonumber(string.sub(str, from, i - 1))if not num then--error(_format('json格式錯誤,不正確的數字, 錯誤位置:{from}', from))endreturn num, i - 1
endlocal function json2string(str, from, to)local ignor = falsefor i = from + 1, to dolocal char = string.sub(str, i, i)if not ignor thenif char == '\"' thenreturn string.sub(str, from + 1, i - 1), ielseif char == '\\' thenignor = trueendelseignor = falseendend--error(_format('json格式錯誤,字符串沒有找到結尾, 錯誤位置:{from}', from))
endlocal function json2array(str, from, to)local result = {}from = from or 1local pos = from + 1local to = to or string.len(str)while (pos <= to) dolocal char = string.sub(str, pos, pos)if char == '\"' thenresult[#result + 1], pos = json2string(str, pos, to)--[[ elseif char == ' ' thenelseif char == ':' thenelseif char == ',' then]]elseif char == '[' thenresult[#result + 1], pos = json2array(str, pos, to)elseif char == '{' thenresult[#result + 1], pos = json2table(str, pos, to)elseif char == ']' thenreturn result, poselseif (char == 'f' or char == 'F') thenresult[#result + 1], pos = json2false(str, pos, to)elseif (char == 't' or char == 'T') thenresult[#result + 1], pos = json2true(str, pos, to)elseif (char == 'n') thenresult[#result + 1], pos = json2null(str, pos, to)elseif (char == 'N') thenresult[#result + 1], pos = json2nan(str, pos, to)elseif numberchars[char] thenresult[#result + 1], pos = json2number(str, pos, to)endpos = pos + 1end--error(_format('json格式錯誤,表沒有找到結尾, 錯誤位置:{from}', from))
endfunction _G.json2table(str, from, to)local result = {}from = from or 1local pos = from + 1local to = to or string.len(str)local keywhile (pos <= to) dolocal char = string.sub(str, pos, pos)if char == '\"' thenif not key thenkey, pos = json2string(str, pos, to)elseresult[key], pos = json2string(str, pos, to)key = nilend--[[ elseif char == ' ' thenelseif char == ':' thenelseif char == ',' then]]elseif char == '[' thenif not key thenkey, pos = json2array(str, pos, to)elseresult[key], pos = json2array(str, pos, to)key = nilendelseif char == '{' thenif not key thenkey, pos = json2table(str, pos, to)elseresult[key], pos = json2table(str, pos, to)key = nilendelseif char == '}' thenreturn result, poselseif (char == 'f' or char == 'F') thenresult[key], pos = json2false(str, pos, to)key = nilelseif (char == 't' or char == 'T') thenresult[key], pos = json2true(str, pos, to)key = nilelseif (char == 'n') thenresult[key], pos = json2null(str, pos, to)key = nilelseif (char == 'N') thenresult[key], pos = json2nan(str, pos, to)key = nilelseif numberchars[char] thenif not key thenkey, pos = json2number(str, pos, to)elseresult[key], pos = json2number(str, pos, to)key = nilendendpos = pos + 1end--error(_format('json格式錯誤,表沒有找到結尾, 錯誤位置:{from}', from))
end--json格式中表示字符串不能使用單引號
local jsonfuncs = {['\"'] = json2string,['['] = json2array,['{'] = json2table,['f'] = json2false,['F'] = json2false,['t'] = json2true,['T'] = json2true,
}function _G.json2lua(str)local char = string.sub(str, 1, 1)local func = jsonfuncs[char]if func thenreturn func(str, 1, string.len(str))endif numberchars[char] thenreturn json2number(str, 1, string.len(str))end
end--打印table的函數
function debug.dump(obj)local getIndent, quoteStr, wrapKey, wrapVal, isArray, dumpObjgetIndent = function(level)return string.rep("\t", level)endquoteStr = function(str)str = string.gsub(str, "[%c\\\"]", {["\t"] = "\\t",["\r"] = "\\r",["\n"] = "\\n",["\""] = "\\\"",["\\"] = "\\\\",})return '"' .. str .. '"'endwrapKey = function(val)if type(val) == "number" thenreturn "[" .. val .. "]"elseif type(val) == "string" thenreturn "[" .. quoteStr(val) .. "]"elsereturn "[" .. tostring(val) .. "]"endendwrapVal = function(val, level)if type(val) == "table" thenreturn dumpObj(val, level)elseif type(val) == "number" thenreturn valelseif type(val) == "string" thenreturn quoteStr(val)elsereturn tostring(val)endendlocal isArray = function(arr)local count = 0for k, v in pairs(arr) docount = count + 1endfor i = 1, count doif arr[i] == nil thenreturn falseendendreturn true, countenddumpObj = function(obj, level)if type(obj) ~= "table" thenreturn wrapVal(obj)endlevel = level + 1local tokens = {}tokens[#tokens + 1] = "{"local ret, count = isArray(obj)if ret thenfor i = 1, count dotokens[#tokens + 1] = getIndent(level) .. wrapVal(obj[i], level) .. ","endelsefor k, v in pairs(obj) dotokens[#tokens + 1] = getIndent(level) .. wrapKey(k) .. " = " .. wrapVal(v, level) .. ","endendtokens[#tokens + 1] = getIndent(level - 1) .. "}"return table.concat(tokens, "\n")endreturn dumpObj(obj, 0)
end--將mrcp返回的結果轉化成lua可用的jsonlua play_and_detect_speech
function makeXml2Json(str)str = string.sub(str, 21, #str - 1)resultJson = json2lua(str)return resultJson
endfunction TTSXML(message)grammar = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"grammar = grammar .. "<speak version=\"1.0\" xml:lang=\"zh-cn\">"grammar = grammar .. messagegrammar = grammar .. "</speak>"return grammar
endfunction initGrammarXML()times = os.time()grammar = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"grammar = grammar .. "<grammar xmlns=\"http://www.w3.org/2001/06/grammar\" xml:lang=\"fr\" version=\"1.0\" root=\"service\">"grammar = grammar .. "<rule id=\"service\">"grammar = grammar .. "<one-of>"grammar = grammar .. "<item><ruleref uri=\"#voice-guide\"/></item>"grammar = grammar .. "</one-of>"grammar = grammar .. "</rule>"grammar = grammar .. "<rule id=\"domain\">"grammar = grammar .. "<one-of>"grammar = grammar .. "<item><ruleref uri=\"#common\"/></item>"grammar = grammar .. "</one-of>"grammar = grammar .. "</rule>"grammar = grammar .. "<rule id=\"need-qa\">"grammar = grammar .. "<one-of>"grammar = grammar .. "<item>{\"regular\":{\"qa\":{\"address\":\"" .. brokerAddress .. "/queryAction\",\"robot\":\"" .. robothashcode .. "\",\"channel\":\"" .. channelId .. "\",\"appKey\":\"" .. appKey .. "\", \"protocolId\": 5, \"talkerId\": \"" .. talkerId .. "\",\"receiverId\": \"" .. receiverId .. "\",\"type\": \"voice\",\"isNeedClearHistory\": 0, \"isQuestionQuery\": 0, \"userId\":\"" .. callUserId .. "\",\"time\":" .. times .. ",\"msgId\":\"0\"},\"result\":\"singleNode.answerMsg,singleNode.cmd,answerTypeId\"}}</item>"grammar = grammar .. "</one-of>"grammar = grammar .. "</rule>"grammar = grammar .. "</grammar>\r\n\r\n"return grammar
end--向機器人發送文字
function sendMessage2NLU(message)printLog("-----renrendai---sendMessage2NLU#message-----" .. call_num .. "--" .. message)local response_body = {}times = os.time()local reqbody = 'query=' .. message .. '&protocolId=5&userId=' .. callUserId .. '&receiverId=' .. receiverId .. '&talkerId=' .. talkerId .. '&platformConnType=' .. channelId .. '&appKey=' .. appKey .. '&robotHashCode=' .. robothashcode;res, code = http.request {url = "http://10.248.17.3:8080/CSRBroker/queryAction",method = "POST",headers ={["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8",["Content-Length"] = string.len(reqbody)},source = ltn12.source.string(reqbody),sink = ltn12.sink.table(response_body)}return table.concat(response_body);
endfunction replaceChar(word)word = word.gsub(word, """, "\"");word = word.gsub(word, "
", "\r\n");return word
end--taskId
local taskId = "";
--姓名
local name = "";
--記錄是否使用TTS播報
local useTTS = true;
--記錄交互次數 初始值為2表示包含進入場景時的兩次文本信息交互
local interCount = 0;
--行動碼
local actionCode = ""
--逾期天數
local overdueDay = ""
--識別錯誤次數
local errorNum = 0
--記錄沒有說話的次數
local noSpeakNum = 0;
--播報語
local prompt = ""
--返回結果的次數,規定值返回一次
local updateTime = 0
--開始時間
local createdTime = "";
local defaultCode = "ERROR"
local isOver = false
--出錯時無法理解的情況
local errReqPrompt = "喂,你好,這里是人貸借款,您在我司平臺還有尚未處理的賬單,請盡快還款,如果您已經還款了,請忽略本次提醒,祝您生活愉快,再見。"--異常處理話術
local notFoundMsg = "請您再說一遍,我沒明白"
--查詢場景
function getStrategyMessage()printLog("-----renrendai---getStrategyMessage()----phone:" .. call_num .. "------transparam:" .. transparam)response_body = {}times = os.time()local reqbody = '{\"userId\":'..userId..',\"phone\":'..call_num..',\"transparam\":'..transparam..'}';--local req_url = '..getUserInfoUrl..'\"?phone=\"' ..call_num.. '\"&transparam=\"' ..transparam.. ''printLog("----0926----getStrategyMessage()###reqbody:" .. reqbody)printLog("----0926----getStrategyMessage()###getCustomerInfo:" .. getCustomerInfo)local res, code = http.request {url = getCustomerInfo, --客戶信息接口method = "POST",headers ={["Content-Type"] = "application/json;charset=utf-8",["Content-Length"] = string.len(reqbody)},source = ltn12.source.string(reqbody),sink = ltn12.sink.table(response_body)}printLog("---0926---resbody: " .. table.concat(response_body))getInfoResult = json2table(response_body[1])if (getInfoResult.rtnCode == "0000") thenstrategyResult = getInfoResult.beanstrategy = strategyResult.strategycallobject = strategyResult.callobjectuname = strategyResult.unameproduct = strategyResult.productcompany = strategyResult.companytransparam = strategyResult.transparamgender = strategyResult.genderoverdueday = strategyResult.overduedayrepayable = strategyResult.repayablecsphone = strategyResult.csphonecreatedTime = strategyResult.createdtimerobothashcode = strategyResult.rebotkeytaskId = strategyResult.taskIdremark = strategyResult.remarkextend1 = strategyResult.extend1extend2 = strategyResult.extend2extend3 = strategyResult.extend3extend4 = strategyResult.extend4printLog("---0926---" .. uname .. "---" .. call_num .. "---robothashcode--" .. robothashcode)sendMessage2NLU(strategy)--sendMessage2NLU("產品類型人貸撥打對象本人欠款人姓名朱瑞欠款人性別女欠款金額1000逾期天數1客服熱線4008006800")sendMessage2NLU("產品類型" .. product .. "撥打對象" .. callobject .. "欠款人姓名" .. uname .. "欠款人性別" .. gender .. "欠款金額" .. repayable .. "逾期天數" .. overdueday .. "客服熱線" .. csphone)elseprintLog("------ 獲取策略異常!------")session:speak(errReqPrompt)--獲取信息失敗直接更新,并掛斷電話forceEndHandle()end
end--更新通話結果
function updateResultInfo(actionCode)printLog("-----renrendai---updateResultInfo#actioninfo()-----taskId:" .. taskId .. "--phone:" .. call_num .. "--transparam:" .. transparam .. "---callResult:" .. actionCode)response_body = {}times = os.time()endTime = os.date("%Y-%m-%d %H:%M:%S", os.time())printLog("----0926----updateResultInfo()###endTime:" .. endTime)local reqbody = '{\"userId\":'..userId..',\"taskId\":\"' .. taskId .. '\",\"phone\":\"' .. call_num .. '\",\"transparam\":\"' .. transparam .. '\",\"callResult\":\"' .. actionCode .. '\",\"outCalls\":\"' .. interCount .. '\",\"startTime\":\"' .. startTime .. '\",\"endTime\":\"' .. endTime .. '\",\"outFile\":\"' .. record_file .. '\"}';printLog("----0926----updateResultInfo()###reqbody:" .. reqbody)res, code = http.request {url = updateUserInfoUrl,method = "POST",headers ={["Content-Type"] = "application/json;charset=utf-8",["Content-Length"] = string.len(reqbody)},source = ltn12.source.string(reqbody),sink = ltn12.sink.table(response_body)}printLog("---0926---updateResultInfo ##resbody: " .. table.concat(response_body))getUpdateResult = json2table(response_body[1])if (getUpdateResult.rtnCode == "0000") thenprintLog("---0926---" .. getUpdateResult.rtnMsg .. "---更新成功!---")elseif (getUpdateResult.rtnCode == "8002") thenprintLog("---0926---" .. getUpdateResult.rtnMsg .. "---更新異常!---")elseprintLog("---0926---更新失敗!---")endendfunction toSpeak()matchWav()interCount = interCount + 1if useTTS == true then--session:playAndGetDigits(1,1,1,1,'',"say:unimrcp:"..ttsName..":"..prompt,"",'[0123456789*#]')session:speak(prompt)else--session:playAndGetDigits(1,1,1,1,'',prompt,"",'[0123456789*#]')session:streamFile(prompt)endprintLog("-----renrendai---toSpeak#prompt-----" .. call_num .. "--")
end--強制掛斷流程 比如用戶已經掛電話
function forceEndHandle()printLog("-----renrendai0608---forceEndHandle###call_num-----" .. call_num .. "--updateTime--" .. updateTime .. "---actionCode---" .. actionCode)if (endTime == "") thenendTime = os.date("%Y-%m-%d %H:%M:%S", os.time())endif (updateTime == 0) thenif (string.len(actionCode) == 0) thenupdateResultInfo(defaultCode)updateTime = updateTime + 1elseupdateResultInfo(actionCode)updateTime = updateTime + 1endendsession:hangup()
endfunction textToNluHandle(msg)resultBack = sendMessage2NLU(msg)luaResult = json2table(resultBack)answerTypeId = luaResult.answerTypeIdprompt = luaResult.singleNode.answerMsg--處理不是場景,也即無法理解的情況if (answerTypeId ~= 4) thenisOver = trueendif (answerTypeId == 4) thenif (luaResult.singleNode.cmd ~= nil) thenif (string.len(luaResult.singleNode.cmd) > 0) thendealActionCode(luaResult.singleNode.cmd)endendend
end--處理行動碼 其他時候不返回
--0+行動碼(優先級最高,不掛斷)
--1+行動碼(優先級最高,掛斷)
function dealActionCode(cmdCode)printLog("-----renrendai---textToNluHandle#cmd-----" .. call_num .. "--" .. cmdCode)endCode = string.sub(cmdCode, 1, 1)if (endCode == "0") thenisOver = falseif (string.len(cmdCode) > 1) thenactionCode = string.sub(cmdCode, 2, -1)endelseif (endCode == "1") thenisOver = trueif (string.len(cmdCode) > 1) thenactionCode = string.sub(cmdCode, 2, -1)endelseisOver = falseend
end--mrpc識別用戶語音流
function mrcpInputHandle()--session:execute("detect_speech", "stop")local grammar = initGrammarXML()session:execute("play_and_detect_speech", "say:unimrcp:" .. ttsName .. ":,,,, detect:unimrcp {start-input-timers=true,no-input-timeout=5000,recognition-timeout=10000,Sensitivity-Level=0.13,speech-complete-timeout=250} inline:" .. grammar)xml = session:getVariable('detect_speech_result')if (xml ~= nil) thenxml = replaceChar(xml)endif (xml ~= nil and string.len(xml) > 50) thennoSpeakNum = 0nluResult = parse_xml(xml)[2][1][1][1][1]speechResult = parse_xml(xml)[2][1][2][1]jsonResult = json2table(nluResult)results = jsonResult.results--6,8代表有結果if (results == nil) thenprompt = notFoundMsgendprintLog("-----renrendai---mrcpInputHandle#results[1].answerTypeId-----" .. call_num .. "--" .. results[1].answerTypeId)if (results[1].answerTypeId == 4) thenif (results[1].answerMsg ~= nil) then --airesult中answer不為空時errorNum = 0prompt = results[1].answerMsgif (results[1].cmd ~= nil) thendealActionCode(results[1].cmd)endelseerrorNum = errorNum + 1printLog("-----renrendai---mrcpInputHandle#沒有結果-----" .. call_num .. "--" .. errorNum)prompt = notFoundMsgif (errorNum == 2) thenisOver = trueelseisOver = falseendendelseerrorNum = errorNum + 1printLog("-----renrendai---mrcpInputHandle#無法理解-----" .. call_num .. "--" .. errorNum)prompt = notFoundMsgif (errorNum == 2) thenisOver = trueelseisOver = falseendendelseprintLog("-----renrendai---mrcpInputHandle#靜音-----" .. call_num .. "--" .. noSpeakNum)noSpeakNum = noSpeakNum + 1--記錄靜音的總次數--silenceCount = silenceCount + 1if (noSpeakNum >= 1) thentextToNluHandle("silent")endend
end--主方法
function welcomeBegin()--session:speak("你好,如果聽到這句話,表示TTS聲音正常!")--獲取場景入口getStrategyMessage()--sendMessage2NLU("欠款人姓名王晶")--sendMessage2NLU("策略1")--sendMessage2NLU("產品類型人貸撥打對象本人欠款人姓名朱瑞欠款人性別女欠款金額1000逾期天數1客服熱線4008006800")--開始場景對話textToNluHandle("開始")if (session:ready() ~= true) thenprintLog("-----renrendai---mrcpInputHandle#未循環掛斷-----" .. call_num)forceEndHandle()endwhile (session:ready() == true) do--轉到lua腳本的時候,用戶實際已經接通了,但由于外呼系統可能在用戶尚未接通的時候,就已經把通話轉過來了if (startTime == "") thenstartTime = os.date("%Y-%m-%d %H:%M:%S", os.time())end--播報toSpeak()--判斷是否掛斷 如果是則掛斷if isOver == true thenforceEndHandle()printLog("-----renrendai---mrcpInputHandle#場景需要掛斷-----" .. call_num)breakend--判斷session是否存在if (session:ready() ~= true) thenforceEndHandle()printLog("-----renrendai---mrcpInputHandle#播報后掛斷-----" .. call_num)break;end--監聽用戶說話mrcpInputHandle()--判斷session是否存在if (session:ready() ~= true) thenforceEndHandle()printLog("-----renrendai---mrcpInputHandle#循環末掛斷-----" .. call_num)break;endendif (session:ready() ~= true) thenforceEndHandle()printLog("-----renrendai---mrcpInputHandle#循環外掛斷-----" .. call_num)end
end--錄音與TTS播報方法
function matchWav()useTTS = true
endsession:set_tts_params("unimrcp:jtts", ttsName)
welcomeBegin()
總結
以上是生活随笔為你收集整理的基于freeswitch1.6的IVR智能语音机器人交互逻辑lua脚本的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springbooot使用google验
- 下一篇: SaaS应用12原则