微信臨時(shí)素材上傳:從踩坑到優(yōu)雅實(shí)現(xiàn))
1. 臨時(shí)素材上傳到底難在哪企業(yè)微信的臨時(shí)素材上傳看起來(lái)只是調(diào)一個(gè)接口的事但真到業(yè)務(wù)里跑起來(lái)坑比想象中多。我前后在三個(gè)項(xiàng)目里踩過(guò)這個(gè)環(huán)節(jié)從最開始用最樸素的方式硬懟到后來(lái)逐步抽象出一套相對(duì)優(yōu)雅的方案中間交了不少學(xué)費(fèi)。這篇文章就把這些經(jīng)驗(yàn)完整拆開講一遍適合正在對(duì)接企業(yè)微信素材能力、或者已經(jīng)對(duì)接但被各種邊界情況折騰過(guò)的開發(fā)者參考。先把概念理清楚。企業(yè)微信的臨時(shí)素材指的是上傳后只在服務(wù)端保留三天的媒體文件包括圖片、語(yǔ)音、視頻和普通文件。它和永久素材最大的區(qū)別就是生命周期短但勝在上傳接口簡(jiǎn)單、不需要額外的素材管理邏輯。典型使用場(chǎng)景是客服系統(tǒng)里發(fā)送一張工單截圖、審批流里附帶一個(gè)PDF附件、機(jī)器人推送一段語(yǔ)音通知。這些場(chǎng)景的共同特點(diǎn)是文件用完即棄不需要長(zhǎng)期存儲(chǔ)臨時(shí)素材剛好匹配。那難點(diǎn)在哪我總結(jié)下來(lái)主要是四塊第一上傳接口用的是multipart/form-data和普通JSON請(qǐng)求的封裝方式完全不同很多HTTP工具類默認(rèn)不支持或者支持得別扭第二media_id這個(gè)返回值有有效期業(yè)務(wù)側(cè)如果緩存了它過(guò)期后發(fā)送會(huì)直接失敗第三文件類型和大小有硬性限制圖片不能超過(guò)10MB、語(yǔ)音不能超過(guò)2MB且只支持AMR和MP3、視頻不能超過(guò)10MB超了接口直接報(bào)錯(cuò)第四并發(fā)上傳時(shí)如果多個(gè)請(qǐng)求同時(shí)拿同一個(gè)文件流會(huì)出現(xiàn)流被消費(fèi)完導(dǎo)致后續(xù)請(qǐng)求拿到空內(nèi)容的問(wèn)題。這四點(diǎn)里前兩點(diǎn)是認(rèn)知問(wèn)題后兩點(diǎn)是實(shí)打?qū)嵉募夹g(shù)坑。我見過(guò)不少項(xiàng)目在這塊的處理方式是寫一個(gè)工具方法每次要發(fā)文件就現(xiàn)讀現(xiàn)傳傳完拿到media_id立刻用掉。這種方式在低頻場(chǎng)景下沒(méi)問(wèn)題但一旦量上來(lái)比如一天幾千次上傳就會(huì)暴露出重復(fù)上傳、流管理混亂、錯(cuò)誤處理缺失等一系列問(wèn)題。所以“優(yōu)雅”這個(gè)詞在這里不是錦上添花而是有明確的工程價(jià)值——它意味著可復(fù)用、可觀測(cè)、可容錯(cuò)。下面我會(huì)從整體設(shè)計(jì)思路開始一步步拆到具體實(shí)現(xiàn)把每個(gè)決策背后的理由講清楚。你如果只是想快速抄個(gè)能跑的代碼可以直接跳到第三節(jié)但如果你想搞清楚為什么這么設(shè)計(jì)建議從頭看。2. 整體方案設(shè)計(jì)與選型考量2.1 為什么不用現(xiàn)成的SDK企業(yè)微信官方提供了多語(yǔ)言SDKJava、Python、PHP都有。我最初也想過(guò)直接用SDK省事但實(shí)際用下來(lái)發(fā)現(xiàn)兩個(gè)問(wèn)題。一是SDK往往把a(bǔ)ccess_token的管理和素材上傳耦合在一起而access_token本身需要全局緩存和刷新如果每個(gè)上傳請(qǐng)求都去觸發(fā)一次token獲取在高并發(fā)下會(huì)頻繁打企業(yè)微信的接口觸發(fā)頻率限制。二是SDK的封裝層次比較深出問(wèn)題的時(shí)候排查鏈路長(zhǎng)比如上傳失敗你很難快速判斷是token問(wèn)題、網(wǎng)絡(luò)問(wèn)題還是文件本身的問(wèn)題。所以我的選擇是access_token自己管上傳邏輯自己寫只依賴一個(gè)通用的HTTP客戶端。這樣每一層都是透明的出問(wèn)題能快速定位。當(dāng)然這不是說(shuō)SDK不好如果你的項(xiàng)目規(guī)模小、上傳頻率低用SDK完全沒(méi)問(wèn)題。但如果你追求可控性和可觀測(cè)性自己封裝是更穩(wěn)的路。2.2 核心設(shè)計(jì)原則整個(gè)方案我定了三條原則。第一條是流的一次性消費(fèi)文件流從磁盤或網(wǎng)絡(luò)讀進(jìn)來(lái)之后只允許被消費(fèi)一次避免重復(fù)讀取導(dǎo)致的性能損耗和狀態(tài)混亂。第二條是media_id不跨請(qǐng)求緩存每次上傳拿到media_id后立即返回給調(diào)用方由調(diào)用方在本次業(yè)務(wù)邏輯內(nèi)用完即棄不做全局緩存。第三條是錯(cuò)誤分層把網(wǎng)絡(luò)錯(cuò)誤、接口錯(cuò)誤、文件校驗(yàn)錯(cuò)誤分開處理每類錯(cuò)誤有不同的重試策略和日志級(jí)別。這三條原則看起來(lái)簡(jiǎn)單但落地的時(shí)候需要一些設(shè)計(jì)上的取舍。比如第一條如果調(diào)用方傳進(jìn)來(lái)的是一個(gè)InputStream那上傳完之后這個(gè)流就關(guān)了調(diào)用方不能再讀。所以我在接口設(shè)計(jì)上要求調(diào)用方傳文件路徑或者字節(jié)數(shù)組而不是流。字節(jié)數(shù)組適合小文件文件路徑適合大文件這樣內(nèi)部可以自己控制流的生命周期。2.3 模塊劃分整個(gè)上傳能力我拆成了四個(gè)模塊。TokenManager負(fù)責(zé)access_token的獲取、緩存和刷新用雙檢鎖加過(guò)期時(shí)間控制保證多線程下只有一個(gè)線程去刷新。MediaValidator負(fù)責(zé)文件校驗(yàn)包括類型、大小、擴(kuò)展名校驗(yàn)不通過(guò)直接拋業(yè)務(wù)異常不走網(wǎng)絡(luò)請(qǐng)求。MediaUploader是核心上傳邏輯負(fù)責(zé)組裝multipart請(qǐng)求、發(fā)送、解析響應(yīng)。UploadResult是統(tǒng)一的返回結(jié)構(gòu)包含media_id、類型、創(chuàng)建時(shí)間以及一個(gè)isSuccess的標(biāo)志。這四個(gè)模塊之間是單向依賴Uploader依賴Validator和TokenManagerValidator和TokenManager互不依賴。這樣的好處是每個(gè)模塊可以單獨(dú)測(cè)試比如Validator的測(cè)試不需要mock網(wǎng)絡(luò)TokenManager的測(cè)試不需要真實(shí)文件。2.4 關(guān)于multipart的選型multipart/form-data的組裝我試過(guò)三種方式。第一種是手寫boundary拼接字節(jié)流靈活但容易出錯(cuò)尤其是boundary和內(nèi)容里出現(xiàn)沖突的時(shí)候。第二種是用Apache HttpClient的MultipartEntityBuilder封裝好、穩(wěn)定但引入了一個(gè)比較重的依賴。第三種是用OkHttp的MultipartBody輕量、API清晰我最終選了這個(gè)。選OkHttp的理由是它的MultipartBody.Builder用起來(lái)很直觀addFormDataPart可以同時(shí)指定name、filename和RequestBody而且RequestBody可以基于文件或字節(jié)數(shù)組創(chuàng)建天然支持流式上傳。另外OkHttp的連接池和超時(shí)控制也比較成熟不需要自己再造輪子。如果你用的是Java生態(tài)里的其他HTTP客戶端思路是一樣的關(guān)鍵是找到支持流式multipart的API。3. 核心細(xì)節(jié)與實(shí)操要點(diǎn)拆解3.1 access_token的管理策略access_token是企業(yè)微信所有接口的通行證有效期7200秒也就是兩小時(shí)。它的獲取接口有頻率限制而且新獲取的token會(huì)讓舊的失效。這意味著你不能每個(gè)請(qǐng)求都去獲取也不能緩存太久導(dǎo)致過(guò)期。我的做法是用一個(gè)帶過(guò)期時(shí)間的緩存key是應(yīng)用的corpId和secret的組合value是token和過(guò)期時(shí)間戳。獲取token的時(shí)候先查緩存如果存在且距離過(guò)期還有300秒以上直接用否則加鎖去刷新。加鎖用雙檢鎖避免多個(gè)線程同時(shí)刷新。刷新成功后更新緩存失敗則拋異常讓上層決定是否重試。這里有個(gè)細(xì)節(jié)過(guò)期時(shí)間不要設(shè)成7200秒整要留出網(wǎng)絡(luò)延遲和時(shí)鐘偏差的余量。我一般設(shè)成6900秒也就是提前300秒過(guò)期。另外如果企業(yè)微信返回的token過(guò)期時(shí)間字段和預(yù)期不一致以返回的為準(zhǔn)不要硬編碼。注意access_token的緩存不要用本地Map簡(jiǎn)單存多實(shí)例部署的時(shí)候每個(gè)實(shí)例都會(huì)各自刷新導(dǎo)致token頻繁失效。生產(chǎn)環(huán)境建議用Redis之類的集中式緩存或者至少保證同一時(shí)間只有一個(gè)實(shí)例在刷新。3.2 文件校驗(yàn)的邊界條件文件校驗(yàn)看起來(lái)簡(jiǎn)單但邊界條件不少。企業(yè)微信對(duì)臨時(shí)素材的限制是這樣的圖片支持JPG、PNG、GIF、BMP、JPEG大小不超過(guò)10MB語(yǔ)音支持AMR、MP3大小不超過(guò)2MB播放長(zhǎng)度不超過(guò)60秒視頻支持MP4大小不超過(guò)10MB普通文件大小不超過(guò)20MB。校驗(yàn)的時(shí)候要注意幾點(diǎn)。第一擴(kuò)展名和實(shí)際內(nèi)容可能不一致比如一個(gè)exe文件改名為jpg光看擴(kuò)展名是攔不住的。如果業(yè)務(wù)安全要求高需要讀文件頭做魔數(shù)校驗(yàn)。第二大小校驗(yàn)要在讀取文件之前做用File.length()獲取避免把大文件讀進(jìn)內(nèi)存才發(fā)現(xiàn)超限。第三語(yǔ)音的時(shí)長(zhǎng)校驗(yàn)比較麻煩需要解析音頻文件頭如果業(yè)務(wù)不強(qiáng)制可以只校驗(yàn)大小和格式時(shí)長(zhǎng)交給企業(yè)微信接口去判斷。我一般把校驗(yàn)規(guī)則做成配置用枚舉定義每種類型的允許擴(kuò)展名和大小上限這樣新增類型的時(shí)候只改配置不改代碼。校驗(yàn)失敗的時(shí)候拋出帶明確錯(cuò)誤碼的異常比如FILE_TOO_LARGE、UNSUPPORTED_TYPE方便上層做不同的提示。3.3 multipart請(qǐng)求的組裝細(xì)節(jié)組裝multipart請(qǐng)求的時(shí)候有幾個(gè)細(xì)節(jié)容易忽略。第一boundary的生成要用隨機(jī)字符串不要用固定值避免和文件內(nèi)容沖突。OkHttp會(huì)自動(dòng)生成不用自己管。第二Content-Type要設(shè)成multipart/form-data并且?guī)蟗oundary參數(shù)這個(gè)OkHttp的MultipartBody會(huì)自動(dòng)處理。第三每個(gè)part的name要和接口文檔一致企業(yè)微信上傳接口的字段名是media類型是filefilename要帶上原始文件名和擴(kuò)展名。代碼大概長(zhǎng)這樣RequestBody fileBody RequestBody.create(file, MediaType.parse(application/octet-stream)); MultipartBody body new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart(media, file.getName(), fileBody) .build(); Request request new Request.Builder() .url(uploadUrl ?access_token token type type) .post(body) .build();這里type參數(shù)是素材類型取值是image、voice、video、file。注意type要放在URL的query參數(shù)里不是放在body里這是企業(yè)微信接口的設(shè)計(jì)。3.4 響應(yīng)解析與錯(cuò)誤處理企業(yè)微信上傳接口的響應(yīng)是JSON成功的時(shí)候返回media_id、type、created_at。失敗的時(shí)候返回errcode和errmsg。解析的時(shí)候要先判斷HTTP狀態(tài)碼再判斷errcode。有些錯(cuò)誤是HTTP 200但errcode非0比如40001表示token無(wú)效40004表示媒體文件類型不合法40005表示文件大小超限。我的處理方式是HTTP非200直接拋網(wǎng)絡(luò)異常HTTP 200但errcode非0則根據(jù)errcode映射成業(yè)務(wù)異常。40001這種token失效的錯(cuò)誤觸發(fā)一次token刷新后重試一次其他錯(cuò)誤直接拋給上層不重試。重試只做一次避免無(wú)限循環(huán)。提示errcode的映射表建議做成常量類不要散落在代碼里。企業(yè)微信的錯(cuò)誤碼有幾百個(gè)常用的就那幾十個(gè)維護(hù)一份自己的映射表比每次查文檔快得多。3.5 并發(fā)場(chǎng)景下的流管理并發(fā)上傳同一個(gè)文件的時(shí)候如果多個(gè)線程共享一個(gè)InputStream會(huì)出現(xiàn)流被消費(fèi)完的問(wèn)題。我的做法是接口只接受File或byte[]不接受InputStream。如果是File每個(gè)線程自己打開一個(gè)新的FileInputStream如果是byte[]每個(gè)線程自己創(chuàng)建一個(gè)新的ByteArrayInputStream。這樣每個(gè)上傳請(qǐng)求都有獨(dú)立的流互不干擾。如果是大文件用File的方式更省內(nèi)存因?yàn)镺kHttp會(huì)流式讀取文件不會(huì)一次性加載到內(nèi)存。如果是小文件用byte[]更方便因?yàn)榭梢詮?fù)用不需要反復(fù)讀磁盤。我一般設(shè)一個(gè)閾值比如1MB以下的用byte[]以上的用File。4. 完整實(shí)操流程與關(guān)鍵環(huán)節(jié)4.1 環(huán)境準(zhǔn)備與依賴引入先說(shuō)一下環(huán)境。我用的是Java 8加OkHttp 4.x構(gòu)建工具是Maven。依賴只需要兩個(gè)OkHttp和Jackson前者做HTTP請(qǐng)求后者解析JSON。如果你用的是Gradle換成對(duì)應(yīng)的坐標(biāo)就行。dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.9.3/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.0/version /dependencyOkHttp 4.x需要Kotlin標(biāo)準(zhǔn)庫(kù)Maven會(huì)自動(dòng)帶上不用手動(dòng)加。如果你用的是Java 11以上也可以考慮用Java自帶的HttpClient但multipart的支持不如OkHttp方便需要自己拼boundary。4.2 TokenManager的實(shí)現(xiàn)TokenManager的核心是一個(gè)帶過(guò)期時(shí)間的緩存和一個(gè)刷新鎖。我用ConcurrentHashMap存tokenkey是corpId加secretvalue是一個(gè)包含token和過(guò)期時(shí)間的對(duì)象。獲取token的方法先查緩存判斷是否過(guò)期過(guò)期則加鎖刷新。public String getToken(String corpId, String secret) { String key corpId : secret; TokenCache cache tokenMap.get(key); if (cache ! null !cache.isExpired()) { return cache.getToken(); } synchronized (this) { cache tokenMap.get(key); if (cache ! null !cache.isExpired()) { return cache.getToken(); } String token fetchTokenFromApi(corpId, secret); tokenMap.put(key, new TokenCache(token, System.currentTimeMillis() 6900 * 1000)); return token; } }fetchTokenFromApi就是調(diào)企業(yè)微信的gettoken接口解析返回的access_token和expires_in。注意expires_in是秒要轉(zhuǎn)成毫秒。如果接口返回errcode非0拋異常。注意synchronized鎖的是this如果TokenManager是單例沒(méi)問(wèn)題如果是多例鎖不住。生產(chǎn)環(huán)境建議用分布式鎖或者保證TokenManager是單例。4.3 MediaValidator的實(shí)現(xiàn)Validator的職責(zé)是校驗(yàn)文件是否符合企業(yè)微信的要求。我用一個(gè)枚舉定義每種類型的規(guī)則public enum MediaType { IMAGE(image, Arrays.asList(jpg, jpeg, png, gif, bmp), 10 * 1024 * 1024), VOICE(voice, Arrays.asList(amr, mp3), 2 * 1024 * 1024), VIDEO(video, Arrays.asList(mp4), 10 * 1024 * 1024), FILE(file, Collections.emptyList(), 20 * 1024 * 1024); private final String type; private final ListString allowedExtensions; private final long maxSize; // 構(gòu)造方法和getter省略 }校驗(yàn)方法接收File和MediaType先判斷文件是否存在再判斷大小再判斷擴(kuò)展名。FILE類型不限制擴(kuò)展名只限制大小。校驗(yàn)不通過(guò)拋MediaValidationException帶上錯(cuò)誤碼。4.4 MediaUploader的核心邏輯Uploader是核心流程是校驗(yàn)文件、獲取token、組裝multipart、發(fā)送請(qǐng)求、解析響應(yīng)、返回結(jié)果。我把它寫成一個(gè)方法接收File、MediaType、corpId、secret返回UploadResult。public UploadResult upload(File file, MediaType type, String corpId, String secret) { validator.validate(file, type); String token tokenManager.getToken(corpId, secret); String url UPLOAD_URL ?access_token token type type.getType(); RequestBody fileBody RequestBody.create(file, MediaType.parse(application/octet-stream)); MultipartBody body new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart(media, file.getName(), fileBody) .build(); Request request new Request.Builder().url(url).post(body).build(); try (Response response client.newCall(request).execute()) { String responseBody response.body().string(); return parseResponse(responseBody, type); } catch (IOException e) { throw new MediaUploadException(NETWORK_ERROR, e); } }parseResponse解析JSON如果errcode是0返回成功的UploadResult如果是40001刷新token后重試一次其他錯(cuò)誤拋異常。4.5 重試機(jī)制的實(shí)現(xiàn)重試只針對(duì)token失效和網(wǎng)絡(luò)超時(shí)兩種情況。token失效的重試邏輯是捕獲到40001錯(cuò)誤后強(qiáng)制刷新token重新組裝請(qǐng)求再發(fā)一次。網(wǎng)絡(luò)超時(shí)的重試是捕獲IOException后判斷是否是超時(shí)是則重試一次不是則直接拋。重試次數(shù)我設(shè)成1次也就是最多發(fā)兩次請(qǐng)求。為什么不設(shè)更多因?yàn)槠髽I(yè)微信的接口本身有頻率限制重試太多會(huì)觸發(fā)限流反而更慢。而且如果是文件本身的問(wèn)題重試多少次都沒(méi)用。private UploadResult uploadWithRetry(File file, MediaType type, String corpId, String secret, int retryCount) { try { return doUpload(file, type, corpId, secret); } catch (TokenExpiredException e) { if (retryCount 0) { tokenManager.refresh(corpId, secret); return uploadWithRetry(file, type, corpId, secret, retryCount - 1); } throw e; } catch (IOException e) { if (retryCount 0 isTimeout(e)) { return uploadWithRetry(file, type, corpId, secret, retryCount - 1); } throw new MediaUploadException(NETWORK_ERROR, e); } }4.6 調(diào)用示例與結(jié)果處理調(diào)用的時(shí)候業(yè)務(wù)代碼只需要三行MediaUploader uploader new MediaUploader(); UploadResult result uploader.upload(new File(/path/to/image.jpg), MediaType.IMAGE, corpId, secret); String mediaId result.getMediaId();拿到mediaId后立即在本次業(yè)務(wù)邏輯里用掉比如調(diào)發(fā)送消息接口。不要存到數(shù)據(jù)庫(kù)或者緩存里因?yàn)槿旌缶褪Я?。如果業(yè)務(wù)需要長(zhǎng)期保存應(yīng)該把文件存到自己的對(duì)象存儲(chǔ)需要發(fā)送的時(shí)候再上傳一次。提示media_id的有效期是三天但實(shí)際使用中建議按兩天半來(lái)規(guī)劃留出余量。如果業(yè)務(wù)邏輯比較復(fù)雜從上傳到發(fā)送之間可能間隔很久那就要考慮重新上傳。5. 常見問(wèn)題與排查技巧實(shí)錄5.1 上傳失敗排查速查表錯(cuò)誤現(xiàn)象可能原因排查方法解決方案返回40001token無(wú)效或過(guò)期檢查token緩存是否過(guò)期是否被其他實(shí)例刷新覆蓋刷新token后重試檢查緩存策略返回40004文件類型不合法檢查擴(kuò)展名和實(shí)際類型是否匹配修正文件類型或做魔數(shù)校驗(yàn)返回40005文件大小超限檢查文件實(shí)際大小壓縮文件或分片上傳返回41001缺少access_token參數(shù)檢查URL拼接確認(rèn)token不為空連接超時(shí)網(wǎng)絡(luò)問(wèn)題或企業(yè)微信側(cè)限流檢查網(wǎng)絡(luò)連通性查看是否觸發(fā)頻率限制增加超時(shí)時(shí)間降低并發(fā)流被關(guān)閉多線程共享InputStream檢查是否傳入了共享流改用File或byte[]中文文件名亂碼編碼問(wèn)題檢查filename的編碼用URLEncoder編碼文件名5.2 我踩過(guò)的三個(gè)坑第一個(gè)坑是token緩存用了本地Map結(jié)果多實(shí)例部署的時(shí)候A實(shí)例刷新了tokenB實(shí)例還在用舊的導(dǎo)致間歇性40001。后來(lái)改成Redis集中緩存問(wèn)題解決。這個(gè)坑的教訓(xùn)是任何有狀態(tài)的東西在多實(shí)例環(huán)境下都要考慮一致性問(wèn)題。第二個(gè)坑是文件校驗(yàn)只看了擴(kuò)展名結(jié)果有個(gè)用戶上傳了一個(gè)改名為jpg的文本文件企業(yè)微信返回40004。后來(lái)加了魔數(shù)校驗(yàn)讀文件前幾個(gè)字節(jié)判斷真實(shí)類型。魔數(shù)校驗(yàn)的規(guī)則是JPG開頭是FFD8PNG開頭是89504E47GIF開頭是47494638。讀前8個(gè)字節(jié)就夠了。第三個(gè)坑是并發(fā)上傳同一個(gè)文件時(shí)多個(gè)線程共享了一個(gè)FileInputStream導(dǎo)致部分請(qǐng)求拿到空內(nèi)容。后來(lái)改成每個(gè)請(qǐng)求自己打開流問(wèn)題解決。這個(gè)坑的教訓(xùn)是流是有狀態(tài)的不能跨線程共享。5.3 性能優(yōu)化的幾個(gè)點(diǎn)如果上傳頻率很高有幾個(gè)優(yōu)化點(diǎn)可以考慮。第一用連接池OkHttp默認(rèn)有連接池復(fù)用TCP連接減少握手開銷。第二大文件用流式上傳不要讀進(jìn)內(nèi)存OkHttp的RequestBody.create(File)就是流式的。第三token緩存用集中式緩存避免每個(gè)實(shí)例都去刷新。第四批量上傳的時(shí)候用異步OkHttp的enqueue方法可以并發(fā)發(fā)送但要注意控制并發(fā)數(shù)避免觸發(fā)限流。我實(shí)測(cè)下來(lái)單實(shí)例每秒上傳10個(gè)1MB的文件CPU和內(nèi)存都很平穩(wěn)。如果超過(guò)這個(gè)量建議做隊(duì)列削峰把上傳請(qǐng)求放到隊(duì)列里慢慢消費(fèi)。5.4 日志與監(jiān)控上傳這種外部依賴的操作日志一定要打全。我一般記錄這幾個(gè)字段請(qǐng)求ID、文件大小、文件類型、耗時(shí)、結(jié)果碼。請(qǐng)求ID用于串聯(lián)上下游日志文件大小和類型用于分析失敗分布耗時(shí)用于發(fā)現(xiàn)性能問(wèn)題結(jié)果碼用于統(tǒng)計(jì)成功率。監(jiān)控方面我會(huì)統(tǒng)計(jì)上傳成功率、平均耗時(shí)、失敗原因分布。成功率低于99%就告警平均耗時(shí)超過(guò)2秒也告警。失敗原因分布能幫你快速定位是token問(wèn)題還是文件問(wèn)題。注意日志里不要打media_id的完整值打前幾位就行避免泄露。也不要打access_token這個(gè)是敏感信息。5.5 關(guān)于素材類型的幾個(gè)疑問(wèn)有人問(wèn)過(guò)圖片能不能當(dāng)文件上傳可以type傳file就行但這樣企業(yè)微信不會(huì)做圖片處理發(fā)送的時(shí)候也是當(dāng)文件發(fā)。反過(guò)來(lái)文件能不能當(dāng)圖片上傳不行企業(yè)微信會(huì)校驗(yàn)實(shí)際類型不匹配會(huì)返回40004。還有人問(wèn)視頻的封面怎么處理企業(yè)微信上傳視頻接口支持傳封面但臨時(shí)素材接口不支持只有永久素材接口才支持。如果業(yè)務(wù)需要封面要么用永久素材要么自己在發(fā)送的時(shí)候處理。語(yǔ)音的時(shí)長(zhǎng)限制是60秒這個(gè)是在上傳的時(shí)候校驗(yàn)的超過(guò)會(huì)返回錯(cuò)誤。如果你不確定音頻時(shí)長(zhǎng)可以用FFmpeg之類的工具先探測(cè)一下或者干脆只校驗(yàn)大小讓企業(yè)微信去判斷。6. 一些個(gè)人體會(huì)這套方案我在兩個(gè)項(xiàng)目里用過(guò)整體穩(wěn)定性不錯(cuò)。最大的感受是對(duì)接外部接口優(yōu)雅的關(guān)鍵不在于代碼寫得多漂亮而在于對(duì)邊界情況的覆蓋。token過(guò)期、文件超限、網(wǎng)絡(luò)抖動(dòng)、并發(fā)沖突這些才是真正消耗精力的地方。把這些問(wèn)題處理好了代碼自然就優(yōu)雅了。另外一點(diǎn)是不要過(guò)度設(shè)計(jì)。我見過(guò)有的項(xiàng)目為了上傳一個(gè)文件搞了一套復(fù)雜的責(zé)任鏈加策略模式結(jié)果維護(hù)成本比收益還高。我的建議是先跑通再抽象。等你在三個(gè)地方寫了重復(fù)的上傳邏輯再考慮抽成公共模塊。過(guò)早抽象比不抽象更可怕。最后分享一個(gè)小技巧如果你不確定企業(yè)微信的接口行為可以用curl先手動(dòng)調(diào)一次把請(qǐng)求和響應(yīng)都打出來(lái)看。比在代碼里調(diào)試快得多。比如上傳接口你可以這樣調(diào)curl -X POST https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_tokenYOUR_TOKENtypeimage \ -F media/path/to/image.jpg把YOUR_TOKEN換成真實(shí)的token就能看到完整的響應(yīng)。這個(gè)方式在排查40004、40005這類錯(cuò)誤的時(shí)候特別有用因?yàn)槟隳苤苯涌吹狡髽I(yè)微信返回的errmsg比看代碼猜快多了。