title: 微信 JS-SDK 開發實現錄音和圖片上傳功能
date: 2018-08-13 19:00:00
toc: true #是否顯示目錄 table of contents
tags:html
現有一個 .NET 開發的 Wap 網站項目,須要用到錄音和圖片上傳功能。目前該網站只部署在公衆號中使用,而且手機錄音功能的實現只能依賴於微信的接口(詳見另外一篇文章《HTML5 實現手機原生功能》),另外採用即時拍照來上傳圖片的功能也只能調用微信接口才能實現,因此本文簡要地記錄下後端 .NET 、前端 H5 開發的網站如何調用微信接口實現錄音和即時拍照上傳圖片功能。前端
一、(必須配置)打開微信公衆平臺-公衆號設置-功能設置-JS接口安全域名 ,按提示配置。須要將網站發佈到服務器上並綁定域名。加xx.com便可,yy.xx.com也能調用成功。web
JS接口安全域名ajax
設置JS接口安全域名後,公衆號開發者可在該域名下調用微信開放的JS接口。
注意事項:
一、可填寫三個域名或路徑(例:wx.qq.com或wx.qq.com/mp),需使用字母、數字及「-」的組合,不支持IP地址、端口號及短鏈域名。
二、填寫的域名須經過ICP備案的驗證。
三、 將文件 MP_verify_iBVYET3obIwgppnr.txt(點擊下載)上傳至填寫域名或路徑指向的web服務器(或虛擬主機)的目錄(若填寫域名,將文件放置在域名根目錄下,例如wx.qq.com/MP_verify_iBVYET3obIwgppnr.txt;若填寫路徑,將文件放置在路徑目錄下,例如wx.qq.com/mp/MP_verify_iBVYET3obIwgppnr.txt),並確保能夠訪問。
四、 一個天然月內最多可修改並保存三次,本月剩餘保存次數:3redis
二、打開微信公衆平臺-公衆號設置-功能設置-網頁受權域名,按提示配置(這一步在當前開發需求中可能不須要)。算法
三、(必須配置)打開微信公衆平臺-基本配置-公衆號開發信息-IP白名單,配置網頁服務器的公網IP(經過開發者IP及密碼調用獲取access_token 接口時,須要設置訪問來源IP爲白名單)。json
微信開發者工具方便公衆號網頁和微信小程序在PC上進行調試,下載地址 ,說明文檔 。小程序
參考微信公衆平臺技術文檔,其中的《附錄1-JS-SDK使用權限簽名算法》(關於受權的文檔找不到了)。c#
/// <summary> /// 獲取AccessToken /// 參考 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183 /// </summary> /// <returns>access_toke</returns> public string GetAccessToken() { var cache = JCache<string>.Instance; var config = Server.ServerManage.Config.Weixin; // 獲取並緩存 access_token string access_token = cache.GetOrAdd("access_token", "weixin", (i, j) => { var url = "https://api.weixin.qq.com/cgi-bin/token";// 注意這裏不是用"https://api.weixin.qq.com/sns/oauth2/access_token" var result = HttpHelper.HttpGet(url, "appid=" + config.AppId + "&secret=" + config.AppSecret + "&grant_type=client_credential"); // 正常狀況返回 {"access_token":"ACCESS_TOKEN","expires_in":7200} // 錯誤時返回 {"errcode":40013,"errmsg":"invalid appid"} var token = result.FormJObject(); if (token["errcode"] != null) { throw new JException("微信接口異常access_token:" + (string)token["errmsg"]); } return (string)token["access_token"]; }, new TimeSpan(0, 0, 7200)); return access_token; }
/// <summary> /// 獲取jsapi_ticket /// jsapi_ticket是公衆號用於調用微信JS接口的臨時票據。 /// 正常狀況下,jsapi_ticket的有效期爲7200秒,經過access_token來獲取。 /// 因爲獲取jsapi_ticket的api調用次數很是有限,頻繁刷新jsapi_ticket會致使api調用受限,影響自身業務,開發者必須在本身的服務全局緩存jsapi_ticket 。 /// </summary> public string GetJsapiTicket() { var cache = JCache<string>.Instance; var config = Server.ServerManage.Config.Weixin; // 獲取並緩存 jsapi_ticket string jsapi_ticket = cache.GetOrAdd("jsapi_ticket", "weixin", (k, r) => { var access_token = GetAccessToken(); var url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"; var result = HttpHelper.HttpGet(url, "access_token=" + access_token + "&type=jsapi"); // 返回格式 {"errcode":0,"errmsg":"ok","ticket":"字符串","expires_in":7200} var ticket = result.FormJObject(); if ((string)ticket["errmsg"] != "ok") { throw new JException("微信接口異常ticket:" + (string)ticket["errmsg"]); } return (string)ticket["ticket"]; }, new TimeSpan(0, 0, 7200)); return jsapi_ticket; }
/// <summary> /// 微信相關的接口 /// </summary> public class WeixinApi { /// <summary> /// 獲取微信簽名 /// </summary> /// <param name="url">請求的頁面地址</param> /// <param name="config">微信配置</param> /// <returns></returns> public static object GetSignature(string url) { var config = Park.Server.ServerManage.Config.Weixin; var jsapi_ticket = GetJsapiTicket(); // SHA1加密 // 對全部待簽名參數按照字段名的ASCII 碼從小到大排序(字典序)後,使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字符串 var obj = new { jsapi_ticket = jsapi_ticket, //必須與wx.config中的nonceStr和timestamp相同 noncestr = JString.GenerateNonceStr(), timestamp = JString.GenerateTimeStamp(), url = url, // 必須是調用JS接口頁面的完整URL(location.href.split('#')[0]) }; var str = $"jsapi_ticket={obj.jsapi_ticket}&noncestr={obj.noncestr}×tamp={obj.timestamp}&url={obj.url}"; var signature = FormsAuthentication.HashPasswordForStoringInConfigFile(str, "SHA1"); return new { appid = config.AppId, noncestr = obj.noncestr, timestamp = obj.timestamp, signature = signature, }; } }
/// <summary> /// 微信JS-SDK接口 /// </summary> public class WeixinController : BaseController { /// <summary> /// 獲取微信簽名 /// </summary> /// <param name="url">請求的頁面地址</param> /// <returns>簽名信息</returns> [HttpGet] public JResult GetSignature(string url) { return JResult.Invoke(() => { return WeixinApi.GetSignature(url); }); } }
/* -----------------------調用微信JS-SDK接口的JS封裝-------------------------------- */ if (Park && Park.Api) { Park.Weixin = { // 初始化配置 initConfig: function (fn) { // todo: 讀取本地wx.config信息,若沒有或過時則從新請求 var url = location.href.split('#')[0]; Park.get("/Weixin/GetSignature", { url: url }, function (d) { Park.log(d.Data); if (d.Status) { wx.config({ debug: false, // 開啓調試模式,調用的全部api的返回值會在客戶端alert出來,若要查看傳入的參數,能夠在pc端打開,參數信息會經過log打出,僅在pc端時纔會打印。 appId: d.Data.appid,// 必填,公衆號的惟一標識 nonceStr: d.Data.noncestr,// 必填,生成簽名的隨機串 timestamp: d.Data.timestamp,// 必填,生成簽名的時間戳 signature: d.Data.signature,// 必填,簽名,見附錄1 jsApiList: [ // 必填,須要使用的JS接口列表,全部JS接口列表見附錄2 'checkJsApi', 'translateVoice', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'onVoicePlayEnd', 'pauseVoice', 'stopVoice', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'getNetworkType', 'openLocation', 'getLocation', ] }); wx.ready(function () { fn && fn(); }); } }); }, // 初始化錄音功能 initUploadVoice: function (selector, fn) { var voiceUpload = false; // 是否可上傳(避免60秒自動中止錄音和鬆手中止錄音重複上傳) // 用localStorage進行記錄,以前沒有受權的話,先觸發錄音受權,避免影響後續交互 if (!localStorage.allowRecord || localStorage.allowRecord !== 'true') { wx.startRecord({ success: function () { localStorage.allowRecord = 'true'; // 僅僅爲了受權,因此馬上停掉 wx.stopRecord(); }, cancel: function () { alert('用戶拒絕受權錄音'); } }); } var btnRecord = $("" + selector); btnRecord.on('touchstart', function (event) { event.preventDefault(); btnRecord.addClass('hold'); startTime = new Date().getTime(); // 延時後錄音,避免誤操做 recordTimer = setTimeout(function () { wx.startRecord({ success: function () { voiceUpload = true; }, cancel: function () { alert('用戶拒絕受權錄音'); } }); }, 300); }).on('touchend', function (event) { event.preventDefault(); btnRecord.removeClass('hold'); // 間隔過短 if (new Date().getTime() - startTime < 300) { startTime = 0; // 不錄音 clearTimeout(recordTimer); alert('錄音時間過短'); } else { // 鬆手結束錄音 if(voiceUpload){ voiceUpload = false; wx.stopRecord({ success: function (res) { // 上傳到本地服務器 wxUploadVoice(res.localId, fn); }, fail: function (res) { alert(JSON.stringify(res)); } }); } } }); // 微信60秒自動觸發中止錄音 wx.onVoiceRecordEnd({ // 錄音時間超過一分鐘沒有中止的時候會執行 complete 回調 complete: function (res) { voiceUpload = false; alert("錄音時長不超過60秒"); // 上傳到本地服務器 wxUploadVoice(res.localId, fn); } }); }, // 初始化圖片功能 initUploadImage: function (selector, fn, num) { // 圖片上傳功能 // 參考 https://blog.csdn.net/fengqingtao2008/article/details/51469705 // 本地預覽及刪除功能參考 https://www.cnblogs.com/clwhxhn/p/6688571.html $("" + selector).click(function () { wx.chooseImage({ count: num || 1, // 默認9 sizeType: ['original', 'compressed'], // 能夠指定是原圖仍是壓縮圖,默認兩者都有 sourceType: ['album', 'camera'], // 能夠指定來源是相冊仍是相機,默認兩者都有 success: function (res) {//微信返回了一個資源對象 //localIds = res.localIds;//把圖片的路徑保存在images[localId]中--圖片本地的id信息,用於上傳圖片到微信瀏覽器時使用 // 上傳到本地服務器 wxUploadImage(res.localIds, 0, fn); } }); }); }, } } //上傳錄音到本地服務器,並作業務邏輯處理 function wxUploadVoice(localId, fn) { //調用微信的上傳錄音接口把本地錄音先上傳到微信的服務器 //不過,微信只保留3天,而咱們須要長期保存,咱們須要把資源從微信服務器下載到本身的服務器 wx.uploadVoice({ localId: localId, // 須要上傳的音頻的本地ID,由stopRecord接口得到 isShowProgressTips: 1, // 默認爲1,顯示進度提示 success: function (res) { //把錄音在微信服務器上的id(res.serverId)發送到本身的服務器供下載。 $.ajax({ url: Park.getApiUrl('/Weixin/DownLoadVoice'), type: 'get', data: res, dataType: "json", success: function (d) { if (d.Status) { fn && fn(d.Data); } else { alert(d.Msg); } }, error: function (xhr, errorType, error) { console.log(error); } }); }, fail: function (res) { // 60秒的語音這裏報錯:{"errMsg":"uploadVoice:missing arguments"} alert(JSON.stringify(res)); } }); } //上傳圖片到微信,下載到本地,並作業務邏輯處理 function wxUploadImage(localIds, i, fn) { var length = localIds.length; //本次要上傳全部圖片的數量 wx.uploadImage({ localId: localIds[i], //圖片在本地的id success: function (res) {//上傳圖片到微信成功的回調函數 會返回一個媒體對象 存儲了圖片在微信的id //把錄音在微信服務器上的id(res.serverId)發送到本身的服務器供下載。 $.ajax({ url: Park.getApiUrl('/Weixin/DownLoadImage'), type: 'get', data: res, dataType: "json", success: function (d) { if (d.Status) { fn && fn(d.Data); } else { alert(d.Msg); } i++; if (i < length) { wxUploadImage(localIds, i, fn); } }, error: function (xhr, errorType, error) { console.log(error); } }); }, fail: function (res) { alert(JSON.stringify(res)); } }); };
// 頁面上調用微信接口JS Park.Weixin.initConfig(function () { Park.Weixin.initUploadVoice("#voice-dp", function (d) { // 業務邏輯處理(d爲錄音文件在本地服務器的資源路徑) }); Park.Weixin.initUploadImage("#img-dp", function (d) { // 業務邏輯處理(d爲圖片文件在本地服務器的資源路徑) $("#img").append("<img src='" + Park.getImgUrl(d) + "' />"); }) });
// 即第4步中的'/Weixin/DownLoadVoice'和'/Weixin/DownLoadImage'方法的後端實現 /// <summary> /// 微信JS-SDK接口控制器 /// </summary> public class WeixinController : BaseController { /// <summary> /// 下載微信語音文件 /// </summary> /// <link>https://www.cnblogs.com/hbh123/archive/2017/08/15/7368251.html</link> /// <param name="serverId">語音的微信服務器端ID</param> /// <returns>錄音保存路徑</returns> [HttpGet] public JResult DownLoadVoice(string serverId) { return JResult.Invoke(() => { return WeixinApi.GetVoicePath(serverId); }); } /// <summary> /// 下載微信語音文件 /// </summary> /// <link>https://blog.csdn.net/fengqingtao2008/article/details/51469705</link> /// <param name="serverId">圖片的微信服務器端ID</param> /// <returns>圖片保存路徑</returns> [HttpGet] public JResult DownLoadImage(string serverId) { return JResult.Invoke(() => { return WeixinApi.GetImagePath(serverId); }); } }
/// <summary> /// 微信相關的接口實現類 /// </summary> public class WeixinApi { /// <summary> /// 將微信語音保存到本地服務器 /// </summary> /// <param name="serverId">微信錄音ID</param> /// <returns></returns> public static string GetVoicePath(string serverId) { var rootPath = Park.Server.ServerManage.Config.ResPath; string voice = ""; //調用downloadmedia方法得到downfile對象 Stream downFile = DownloadFile(serverId); if (downFile != null) { string fileName = Guid.NewGuid().ToString(); string path = "\\voice\\" + DateTime.Now.ToString("yyyyMMdd"); string phyPath = rootPath + path; if (!Directory.Exists(phyPath)) { Directory.CreateDirectory(phyPath); } // 異步處理(解決當文件稍大時頁面上ajax請求沒有返回的問題) Task task = new Task(() => { //生成amr文件 var armPath = phyPath + "\\" + fileName + ".amr"; using (FileStream fs = new FileStream(armPath, FileMode.Create)) { byte[] datas = new byte[downFile.Length]; downFile.Read(datas, 0, datas.Length); fs.Write(datas, 0, datas.Length); } //轉換爲mp3文件 string mp3Path = phyPath + "\\" + fileName + ".mp3"; JFile.ConvertToMp3(rootPath, armPath, mp3Path); }); task.Start(); voice = path + "\\" + fileName + ".mp3"; } return voice; } /// <summary> /// 將微信圖片保存到本地服務器 /// </summary> /// <param name="serverId">微信圖片ID</param> /// <returns></returns> public static string GetImagePath(string serverId) { var rootPath = Park.Server.ServerManage.Config.ResPath; string image = ""; //調用downloadmedia方法得到downfile對象 Stream downFile = DownloadFile(serverId); if (downFile != null) { string fileName = Guid.NewGuid().ToString(); string path = "\\image\\" + DateTime.Now.ToString("yyyyMMdd"); string phyPath = rootPath + path; if (!Directory.Exists(phyPath)) { Directory.CreateDirectory(phyPath); } //生成jpg文件 var jpgPath = phyPath + "\\" + fileName + ".jpg"; using (FileStream fs = new FileStream(jpgPath, FileMode.Create)) { byte[] datas = new byte[downFile.Length]; downFile.Read(datas, 0, datas.Length); fs.Write(datas, 0, datas.Length); } image = path + "\\" + fileName + ".mp3"; } return image; } /// <summary> /// 下載多媒體文件 /// </summary> /// <param name="media_id"></param> /// <returns></returns> public static Stream DownloadFile(string media_id) { var access_token = GetAccessToken(); string url = "http://file.api.weixin.qq.com/cgi-bin/media/get?"; var action = url + $"access_token={access_token}&media_id={media_id}"; HttpWebRequest myRequest = WebRequest.Create(action) as HttpWebRequest; myRequest.Method = "GET"; myRequest.Timeout = 20 * 1000; HttpWebResponse myResponse = myRequest.GetResponse() as HttpWebResponse; var stream = myResponse.GetResponseStream(); var ct = myResponse.ContentType; // 返回錯誤信息 if (ct.IndexOf("json") >= 0 || ct.IndexOf("text") >= 0) { using (StreamReader sr = new StreamReader(stream)) { // 返回格式 {"errcode":0,"errmsg":"ok"} var json = sr.ReadToEnd().FormJObject(); var errcode = (int)json["errcode"]; // 40001 被其餘地方使用 || 42001 過時 if (errcode == 40001 || errcode == 42001) { // 從新獲取token var cache = Park.Common.JCache.Instance; cache.Remove("access_token", "weixin"); return DownloadFile(media_id); } else { throw new JException(json.ToString()); } } } // 成功接收到數據 else { Stream MyStream = new MemoryStream(); byte[] buffer = new Byte[4096]; int bytesRead = 0; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0) MyStream.Write(buffer, 0, bytesRead); MyStream.Position = 0; return MyStream; } } }
/// <summary> /// 文件處理類 /// </summary> public class JFile { /// <summary> /// 音頻轉換 /// </summary> /// <link>https://www.cnblogs.com/hbh123/p/7368251.html</link> /// <param name="ffmpegPath">ffmpeg文件目錄</param> /// <param name="soruceFilename">源文件</param> /// <param name="targetFileName">目標文件</param> /// <returns></returns> public static string ConvertToMp3(string ffmpegPath, string soruceFilename, string targetFileName) { // 需事先將 ffmpeg.exe 放到 ffmpegPath 目錄下 string cmd = ffmpegPath + @"\ffmpeg.exe -i " + soruceFilename + " -ar 44100 -ab 128k " + targetFileName; return ConvertWithCmd(cmd); } private static string ConvertWithCmd(string cmd) { try { System.Diagnostics.Process process = new System.Diagnostics.Process(); process.StartInfo.FileName = "cmd.exe"; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardInput = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.Start(); process.StandardInput.WriteLine(cmd); process.StandardInput.AutoFlush = true; Thread.Sleep(1000); process.StandardInput.WriteLine("exit"); process.WaitForExit(); string outStr = process.StandardOutput.ReadToEnd(); process.Close(); return outStr; } catch (Exception ex) { return "error" + ex.Message; } } }
微信jssdk錄音功能開發記錄
微信語音上傳下載
ffmpeg.exe 下載地址
狀況1:獲取到 access_token 後,去獲取 jsapi_ticket 時報錯。
access_token 有兩種。第一種是全局的和用戶無關的access_token, 用 appid 和 appsecret 去獲取(/cgi-bin/token)。第二種是和具體用戶有關的,用 appid 和 appsecre 和 code 去獲取 (/sns/oauth2/access_token)。這裏須要的是第一種。
狀況2:完成配置,發起錄音下載錄音到本地時報錯。
緣由:用同一個appid 和 appsecret 去獲取 token,在不一樣的服務器去獲取,致使前一個獲取的token失效。解決方案:在下載錄音到本地時,過濾報錯信息,若爲40001錯誤,則從新獲取token。
獲取簽名,並配置到 wx.config 以後,同時不報這兩個錯誤信息。
解決方案:生成的簽名有誤,注意各個簽名參數的值和生成字符串時的順序。
在微信開發者工具中調試,上傳錄音的方法中從微信下載錄音報錯。
解決方案: 在微信web開發者工具中調試會有這個問題,直接在微信調試則無此問題。
錄製60秒的語音自動中止後,上傳語音到微信服務器報錯,待解決。