傳統意義上來說,想給公眾號增加更多的功能,需要我們有一臺服務器,搭建一個公眾號的後臺服務。那麼在Serverless架構下,我們是否有超簡便的方法來實現一個簡單的公眾號後臺呢?
我說可以:我們只用一個函數,就可以初步的搭建一個公眾號後臺的雛形。
首先我們要有一個公眾號,這一段我就Pass了;
然後我們要學習看文檔:
再然後,我們還要做一個有趣的小操作,那就是為我們的函數申請固定IP:
進入到白名單之後,我們可以填寫表單,完成固定公網出口IP的申請,這裡可能要幾個工作日才能完成。最後就是我們的代碼開發。
1: 想要將函數綁定到公眾號後臺,需要我們參考文檔:
我們可以先在函數中按照文檔完成一個基本的鑑定功能:
<code>def checkSignature(param): ''' :param param: :return: ''' signature = param['signature'] timestamp = param['timestamp'] nonce = param["nonce"] tmparr = [wxtoken, timestamp, nonce] tmparr.sort() tmpstr = ''.join(tmparr) tmpstr = hashlib.sha1(tmpstr.encode("utf-8")).hexdigest() return tmpstr == signature/<code>
再定義一個基本的回覆方法:
<code>def response(body, status=200): return { "isBase64Encoded": False, "statusCode": status, "headers": {"Content-Type": "text/html"}, "body": body }/<code>
然後在函數入口處:
<code>def main_handler(event, context): if 'echostr' in event['queryString']: # 接入時的校驗 return response(event['queryString']['echostr'] if checkSignature(event['queryString']) else False)/<code>
我們接著配置我們Yaml:
<code># serverless.ymlWeixin_GoServerless: component: "@serverless/tencent-scf" inputs: name: Weixin_GoServerless codeUri: ./Admin handler: index.main_handler runtime: Python3.6 region: ap-shanghai description: 公眾號後臺服務器配置 memorySize: 128 timeout: 20 environment: variables: wxtoken: 自定義一個字符串 appid: 暫時不寫 secret: 暫時不寫 events: - apigw: name: Weixin_GoServerless parameters: protocols: - https environment: release endpoints: - path: / method: ANY function: isIntegratedResponse: TRUE/<code>
我們執行代碼,完成部署:
接下來在我們的公眾號後臺,選擇基本配置:
然後選擇修改配置:
在這裡我們要注意:
- URL,寫我們剛才部署完成返回給我們的地址,並且在最後加一個/
- Token,寫我們Yaml中的wxtoken,兩個地方要保持一樣的字符串
- EncodingAESKey,可以點擊隨機生成
- 消息加密方法可以選擇明文
完成之後,我們可以點擊提交:
看到提交成功,就說明我們已經完成了第一步驟的綁定,接下來,我們到函數的後臺:
打開這個固定出口IP,然後看到IP地址之後,複製IP地址:
點擊查看->修改,並將IP地址複製粘貼進來,保存。
同時我們查看開發者ID和密碼:
並將這兩個內容複製粘貼,放到我們環境變量中:
至此,我們完成了一個公眾號後臺服務的綁定。為了方便之後的操作,
我先獲取一下全局變量:
<code>wxtoken = os.environ.get('wxtoken')appid = os.environ.get('appid')secret = os.environ.get('secret')/<code>
接下來對各個模塊進行編輯(本文只提供部分簡單基礎的模塊,更多功能實現可以參考公眾號文檔實現)
1: 獲取AccessToken模塊:
<code>def getAccessToken(): ''' 正常返回:{"access_token":"ACCESS_TOKEN","expires_in":7200} 異常返回:{"errcode":40013,"errmsg":"invalid appid"} :return: ''' url = "公眾號url/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" % (appid, secret) accessToken = json.loads(urllib.request.urlopen(url).read().decode("utf-8")) print(accessToken) return None if "errcode" in accessToken else accessToken["access_token"]/<code>
2: 創建自定義菜單模塊:
<code>def setMenu(menu): ''' 正確返回:{"errcode":0,"errmsg":"ok"} 異常返回:{"errcode":40018,"errmsg":"invalid button name size"} :return: ''' accessToken = getAccessToken() if not accessToken: return "Get Access Token Error" url = "公眾號url/cgi-bin/menu/create?access_token=%s" % accessToken postData = urllib.parse.urlencode(menu).encode("utf-8") requestAttr = urllib.request.Request(url=url, data=postData) responseAttr = urllib.request.urlopen(requestAttr) responseData = json.loads(responseAttr.read()) return responseData['errmsg'] if "errcode" in responseData else "success"/<code>
3: 常見消息回覆模塊:
<code>def textXML(body, event): ''' :param body: {"msg": "test"} msg: 必填,回覆的消息內容(換行:在content中能夠換行,客戶端就支持換行顯示) :param event: :return: ''' return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <content> """.format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), msg=body["msg"])def pictureXML(body, event): ''' :param body: {"media_id": 123} media_id: 必填,通過素材管理中的接口上傳多媒體文件,得到的id。 :param event: :return: ''' return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <image> <mediaid> /<image> """.format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), media_id=body["media_id"])def voiceXML(body, event): ''' :param body: {"media_id": 123} media_id: 必填,通過素材管理中的接口上傳多媒體文件,得到的id :param event: :return: ''' return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <voice> <mediaid> /<voice> """.format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), media_id=body["media_id"])def videoXML(body, event): ''' :param body: {"media_id": 123, "title": "test", "description": "test} media_id: 必填,通過素材管理中的接口上傳多媒體文件,得到的id title::選填,視頻消息的標題 description:選填,視頻消息的描述 :param event: :return: ''' return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <video> <mediaid> <title> <description> /<video> """.format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), media_id=body["media_id"], title=body.get('title', ''), description=body.get('description', ''))def musicXML(body, event): ''' :param body: {"media_id": 123, "title": "test", "description": "test} media_id:必填,縮略圖的媒體id,通過素材管理中的接口上傳多媒體文件,得到的id title:選填,音樂標題 description:選填,音樂描述 url:選填,音樂鏈接 hq_url:選填,高質量音樂鏈接,WIFI環境優先使用該鏈接播放音樂 :param event: :return: ''' return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <music> <title> <description> <musicurl> <hqmusicurl> <thumbmediaid> /<music> """.format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), media_id=body["media_id"], title=body.get('title', ''), url=body.get('url', ''), hq_url=body.get('hq_url', ''), description=body.get('description', ''))def articlesXML(body, event): ''' :param body: 一個list [{"title":"test", "description": "test", "picUrl": "test", "url": "test"}] title:必填,圖文消息標題 description:必填,圖文消息描述 picUrl:必填,圖片鏈接,支持JPG、PNG格式,較好的效果為大圖360*200,小圖200*200 url:必填,點擊圖文消息跳轉鏈接 :param event: :return: ''' if len(body["articles"]) > 8: # 最多隻允許返回8個 body["articles"] = body["articles"][0:8] tempArticle = """<item> <title> <description> <picurl>/<item>""" return """ <tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <articlecount>{count}/<articlecount> <articles> {articles} /<articles> """.format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), count=len(body["articles"]), articles="".join([tempArticle.format( title=eveArticle['title'], description=eveArticle['description'], picurl=eveArticle['picurl'], url=eveArticle['url'] ) for eveArticle in body["articles"]]))/<code>
4: 對main_handler進行修改,使其:
- 識別綁定功能
- 識別基本信息
- 識別特殊額外請求(例如通過url觸發自定義菜單的更新)
整體代碼:
<code>def main_handler(event, context): print('event: ', event) if event["path"] == '/setMenu': # 設置菜單接口 menu = { "button": [ { "type": "view", "name": "精彩文章", "url": "******" }, { "type": "view", "name": "開源項目", "url": "*******" }, { "type": "miniprogram", "name": "在線編程", "appid": "wx453cb539f9f963b2", "pagepath": "/page/index" }] } return response(setMenu(menu)) if 'echostr' in event['queryString']: # 接入時的校驗 return response(event['queryString']['echostr'] if checkSignature(event['queryString']) else False) else: # 用戶消息/事件 event = getEvent(event) if event["MsgType"] == "text": # 文本消息 return response(body=textXML({"msg": "這是一個文本消息"}, event)) elif event["MsgType"] == "image": # 圖片消息 return response(body=textXML({"msg": "這是一個圖片消息"}, event)) elif event["MsgType"] == "voice": # 語音消息 pass elif event["MsgType"] == "video": # 視頻消息 pass elif event["MsgType"] == "shortvideo": # 小視頻消息 pass elif event["MsgType"] == "location": # 地理位置消息 pass elif event["MsgType"] == "link": # 鏈接消息 pass elif event["MsgType"] == "event": # 事件消息 if event["Event"] == "subscribe": # 訂閱事件 if event.get('EventKey', None): # 用戶未關注時,進行關注後的事件推送(帶參數的二維碼) pass else: # 普通關注 pass elif event["Event"] == "unsubscribe": # 取消訂閱事件 pass elif event["Event"] == "SCAN": # 用戶已關注時的事件推送(帶參數的二維碼) pass elif event["Event"] == "LOCATION": # 上報地理位置事件 pass elif event["Event"] == "CLICK": # 點擊菜單拉取消息時的事件推送 pass elif event["Event"] == "VIEW": # 點擊菜單跳轉鏈接時的事件推送 pass/<code>
在上述代碼中可以看到:
<code>if event["MsgType"] == "text": # 文本消息 return response(body=textXML({"msg": "這是一個文本消息"}, event))elif event["MsgType"] == "image": # 圖片消息 return response(body=textXML({"msg": "這是一個圖片消息"}, event))/<code>
這裡就是說,當用戶發送了文本消息時候,我們給用戶回覆一個文本消息:這是一個文本消息,當用戶發送了一個圖片,我們給用戶返回這是一個圖片消息,用這兩個功能測試我們這個後臺的連通性:
可以看到,系統已經可以正常返回。
這樣一個簡單的小框架或者小Demo的意義是什麼呢?
- 可以告訴大家,我們可以很輕量的,通過一個函數來實現公眾號的後端服務;
- 這裡都是基礎能力,我們可以在這個基礎能力基礎上,“肆無忌憚”的添加創新力,例如:
- 用戶傳過來的是圖片消息,我們可以通過一些識圖API告訴用戶這個圖片包括了什麼(接下來的文章分享中會涉及這部分內容)
- 用戶傳過來的是文字消息,我們可以先設定一些幫助信息/檢索信息進行對比,如果沒找到就給用戶開啟聊天功能(這裡涉及到人工智能中的自然語言處理,例如對話、文本相似度檢測,之後分享也會和大家舉例說明)
- 如果用戶發送到是語音我們還可以將其轉成文本,生成對話消息,然後再轉換成語音返回給用戶
- 如果用戶發送了地理位置信息,我們可以返回給用戶所在經緯度的街景信息或者周邊的信息/生活服務信息等
- .........
這些能力都可以自行添加
當然,如果你覺得上面的實現比較Low,也沒有問題,因為這裡還有一個Werobot的框架,有的人比較疑惑:Werobot也能部署在雲函數上?Of Course!
<code>Weixin_Werobot: component: "@serverless/tencent-werobot" inputs: functionName: Weixin_Werobot code: ./test werobotProjectName: app werobotAttrName: robot functionConf: timeout: 10 memorySize: 256 environment: variables: wxtoken: ********* appid: ********* secret: ********* apigatewayConf: protocols: - http environment: release/<code>
然後新建代碼:
<code>import osimport werobotrobot = werobot.WeRoBot(token=os.environ.get('wxtoken'))robot.config['SESSION_STORAGE'] = Falserobot.config["APP_ID"] = os.environ.get('appid')robot.config["APP_SECRET"] = os.environ.get('secret')# @robot.handler 處理所有消息@robot.handlerdef hello(message): return 'Hello World!'if __name__ == "__main__": # 讓服務器監聽在 0.0.0.0:80 robot.config['HOST'] = '0.0.0.0' robot.config['PORT'] = 80 robot.run()/<code>
並且在本地安裝werobot相關依賴,完成之後,執行部署:
並把下面的這個地址複製到公眾號後臺:
開啟調用即可。參考Git:https://github.com/serverless-tencent/tencent-werobot
這裡需要注意的是,我們一定要關掉Session或者將Session改成雲數據庫,不能使用本地文件等,例如關閉Session配置:
<code>robot.config['SESSION_STORAGE'] = False/<code>
閱讀更多 GoServerless 的文章