系統(tǒng)實(shí)戰(zhàn):云開(kāi)發(fā)與云函數(shù)核心設(shè)計(jì))
簡(jiǎn)介面向正在準(zhǔn)備微信小程序畢業(yè)設(shè)計(jì)或課程設(shè)計(jì)的學(xué)生這份資源提供了一套完整的校園失物招領(lǐng)系統(tǒng)項(xiàng)目包含可直接閱讀的源碼工程、說(shuō)明文檔和演示視頻。項(xiàng)目圍繞“設(shè)計(jì)與實(shí)現(xiàn)”展開(kāi)覆蓋了從系統(tǒng)總體設(shè)計(jì)、數(shù)據(jù)庫(kù)設(shè)計(jì)到前端功能模塊落地的全過(guò)程具體涉及失物招領(lǐng)信息展示、論壇交流、公告管理、失物發(fā)布等核心頁(yè)面并配有系統(tǒng)測(cè)試的目的與測(cè)試方法說(shuō)明有助于讀者理解小程序前后端交互及常規(guī)開(kāi)發(fā)規(guī)范。目錄層次清晰可借鑒其中的界面布局與數(shù)據(jù)庫(kù)表設(shè)計(jì)思路。資料打包為rar格式大小約38.82MB內(nèi)含項(xiàng)目源碼、Word版設(shè)計(jì)文檔以及操作演示視頻方便對(duì)照文檔查看代碼結(jié)構(gòu)或按視頻復(fù)現(xiàn)運(yùn)行效果。目前已有120人學(xué)習(xí)適合需要完整項(xiàng)目參考、快速梳理畢業(yè)設(shè)計(jì)框架的開(kāi)發(fā)者。1. 基于微信小程序的校園失物招領(lǐng)系統(tǒng)比想象中更值得做的實(shí)戰(zhàn)項(xiàng)目校園失物招領(lǐng)這個(gè)場(chǎng)景絕大多數(shù)人的第一反應(yīng)是“太簡(jiǎn)單了”。但真去接手一個(gè)基于微信小程序的校園失物招領(lǐng)系統(tǒng)你會(huì)發(fā)現(xiàn)它并不是「發(fā)布信息 展示列表」兩件事就能糊弄過(guò)去的。我見(jiàn)過(guò)太多人把精力花在頁(yè)面好不好看上結(jié)果栽在圖片上傳、狀態(tài)流轉(zhuǎn)、重復(fù)認(rèn)領(lǐng)這些看不見(jiàn)的環(huán)節(jié)上。這個(gè)項(xiàng)目真正的價(jià)值在于它把小程序端、云開(kāi)發(fā)后端、數(shù)據(jù)庫(kù)權(quán)限、審核流程串成了一條完整鏈路是一個(gè)能講清楚“數(shù)據(jù)怎么流動(dòng)”的練手項(xiàng)目。這個(gè)系統(tǒng)適合兩類(lèi)人一類(lèi)是正在做課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)需要源碼和說(shuō)明文檔來(lái)支撐答辯的學(xué)生另一類(lèi)是剛接觸小程序開(kāi)發(fā)想用業(yè)務(wù)閉環(huán)來(lái)驗(yàn)證自己能力的從業(yè)者。它不追求高并發(fā)、不涉及復(fù)雜算法但云函數(shù)怎么寫(xiě)、表結(jié)構(gòu)怎么拆、狀態(tài)機(jī)怎么設(shè)計(jì)這些經(jīng)驗(yàn)和寫(xiě)大項(xiàng)目是相通的。標(biāo)題里提到的源碼、說(shuō)明文檔、演示視頻恰恰是把這個(gè)項(xiàng)目從“能跑”推向“能交付”的三件套缺一不可。2. 從需求到表結(jié)構(gòu)先把失物招領(lǐng)的業(yè)務(wù)閉環(huán)畫(huà)清楚2.1 業(yè)務(wù)閉環(huán)發(fā)布—匹配—認(rèn)領(lǐng)—核銷(xiāo)四個(gè)狀態(tài)缺一不可很多人做失物招領(lǐng)系統(tǒng)第一版只做兩張表一張存失物一張存招領(lǐng)用戶自己刷列表碰運(yùn)氣。這樣做不是不能跑但離“系統(tǒng)”兩個(gè)字差得很遠(yuǎn)。真實(shí)的校園場(chǎng)景里丟東西的人希望快速找到撿到東西的人希望盡快脫手管理員希望減少無(wú)效溝通。這三方的訴求疊加起來(lái)業(yè)務(wù)至少要包含四個(gè)環(huán)節(jié)發(fā)布、匹配、認(rèn)領(lǐng)、核銷(xiāo)。發(fā)布環(huán)節(jié)要區(qū)分「尋物啟事」和「失物招領(lǐng)」兩種類(lèi)型它們的瀏覽邏輯不同、認(rèn)領(lǐng)邏輯也不同。尋物啟事是失主主動(dòng)描述物品特征等撿到的人聯(lián)系失物招領(lǐng)是拾主上傳物品照片等失主來(lái)認(rèn)領(lǐng)。匹配環(huán)節(jié)不能只靠人肉刷新至少要有按關(guān)鍵詞搜索和按分類(lèi)篩選的能力。認(rèn)領(lǐng)環(huán)節(jié)是防冒領(lǐng)的關(guān)鍵不能誰(shuí)點(diǎn)一下“我要認(rèn)領(lǐng)”就把東西拿走必須加一層描述核對(duì)或管理員審核。核銷(xiāo)環(huán)節(jié)則是把物品狀態(tài)從“待認(rèn)領(lǐng)”改成“已完成”同時(shí)記錄操作時(shí)間方便日后追溯。這四個(gè)環(huán)節(jié)落實(shí)在系統(tǒng)里就是一張狀態(tài)流轉(zhuǎn)圖發(fā)布1→ 匹配2→ 認(rèn)領(lǐng)3→ 核銷(xiāo)4。狀態(tài)不能只在代碼里用 if 判斷必須在數(shù)據(jù)庫(kù)里落一個(gè)字段比如status用整數(shù)表示0 為待處理、1 為已認(rèn)領(lǐng)待審核、2 為已核銷(xiāo)、3 為已下架。這樣后續(xù)做列表過(guò)濾、管理員統(tǒng)計(jì)、演示視頻里的流程展示都只需要查這一個(gè)字段。2.2 數(shù)據(jù)庫(kù)設(shè)計(jì)用戶表、物品表、認(rèn)領(lǐng)表、審核日志表的具體字段如果把系統(tǒng)做成微信云開(kāi)發(fā)方案數(shù)據(jù)庫(kù)直接用云開(kāi)發(fā)自帶的 JSON 文檔數(shù)據(jù)庫(kù)不需要自己搭 MySQL。這個(gè)選擇對(duì)畢設(shè)和課設(shè)尤其友好不用買(mǎi)服務(wù)器、不用配域名備案、天然支持微信登錄。但你依然需要像設(shè)計(jì)關(guān)系型數(shù)據(jù)庫(kù)一樣提前把集合表和字段定清楚否則寫(xiě)到一半改數(shù)據(jù)結(jié)構(gòu)云函數(shù)里的查詢邏輯會(huì)跟著返工。我一般會(huì)拆四張核心集合。第一個(gè)是用戶集合users字段包括openid用戶唯一標(biāo)識(shí)云開(kāi)發(fā)下建議直接用云函數(shù)獲取、nickName、avatarUrl、studentId學(xué)號(hào)選填、phone選填用于認(rèn)領(lǐng)聯(lián)系、createTime。注意openid不要暴露在小程序端直接寫(xiě)入而是在云函數(shù)里用cloud.getWXContext().OPENID獲取這樣能防止用戶偽造身份。第二個(gè)是物品集合items也就是系統(tǒng)的核心數(shù)據(jù)。字段設(shè)計(jì)如下itemId我用_id自動(dòng)生成、type1 尋物 / 2 招領(lǐng)、title物品名稱、category分類(lèi)比如校園卡、錢(qián)包、耳機(jī)、書(shū)籍、description詳細(xì)描述這里要引導(dǎo)用戶寫(xiě)特征為認(rèn)領(lǐng)核對(duì)做準(zhǔn)備、images數(shù)組存云存儲(chǔ) fileID 列表、location丟失或拾得地點(diǎn)、time丟失或拾得時(shí)間、status狀態(tài)0 待處理 / 1 待審核 / 2 已完成 / 3 已下架、openid發(fā)布者、contact聯(lián)系方式、createTime。這里有一個(gè)容易被忽略的點(diǎn)time這個(gè)字段名在云開(kāi)發(fā)里做排序時(shí)和前端傳入格式容易出歧義建議直接用happenTime表示丟失或拾得時(shí)間避免和createTime混淆。第三張是認(rèn)領(lǐng)集合claims記錄每一次認(rèn)領(lǐng)申請(qǐng)。字段有itemId、claimOpenid、description認(rèn)領(lǐng)者填寫(xiě)的物品特征描述、status0 待審核 / 1 通過(guò) / 2 拒絕、createTime、reviewTime。第四張是審核日志集合reviews記錄管理員的每一次操作字段包括itemId、actionpass / reject / finish、reviewerOpenid、remark、createTime。日志集合看起來(lái)多余但寫(xiě)說(shuō)明文檔時(shí)它是亮點(diǎn)答辯時(shí)也是交代“系統(tǒng)可追溯性”的抓手。如果你選擇自建后端路線用 MySQL 的話四張表對(duì)應(yīng)關(guān)系不變只是把集合名換成表名把 JSON 字段換成 SQL 字段。下面給一份 MySQL 版的核心建表腳本便于對(duì)照CREATE TABLE items ( id INT PRIMARY KEY AUTO_INCREMENT, type TINYINT NOT NULL COMMENT 1招領(lǐng) 2尋物, title VARCHAR(100) NOT NULL, category VARCHAR(50) DEFAULT , description TEXT, images JSON COMMENT 圖片URL數(shù)組, location VARCHAR(100), happen_time DATETIME, status TINYINT DEFAULT 0 COMMENT 0待處理 1待審核 2已完成 3已下架, openid VARCHAR(64) NOT NULL, contact VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE claims ( id INT PRIMARY KEY AUTO_INCREMENT, item_id INT NOT NULL, openid VARCHAR(64) NOT NULL, description TEXT, status TINYINT DEFAULT 0 COMMENT 0待審核 1通過(guò) 2拒絕, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, review_time DATETIME DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;images字段用 JSON 類(lèi)型存儲(chǔ)能省掉一張附件表這是多數(shù)人實(shí)踐后的常見(jiàn)做法。用 utf8mb4 是為了兼容用戶輸入的生僻字和 Emoji微信昵稱里常有特殊字符這個(gè)坑在聯(lián)調(diào)時(shí)才會(huì)暴露。建表后記得給items.status和items.openid加索引列表查詢和我的發(fā)布頁(yè)都用得到。2.3 圖片存儲(chǔ)選型為什么優(yōu)先用微信云存儲(chǔ)而不是自建文件服務(wù)器校園失物招領(lǐng)系統(tǒng)的圖片存儲(chǔ)是很多初學(xué)者第一道坎。有人圖省事把圖片轉(zhuǎn)成 base64 直接存數(shù)據(jù)庫(kù)結(jié)果列表頁(yè)渲染卡到懷疑人生有人堅(jiān)持自建 Nginx FastDFS折騰兩天還沒(méi)把上傳接口調(diào)通。對(duì)這個(gè)項(xiàng)目體量來(lái)說(shuō)微信云開(kāi)發(fā)的云存儲(chǔ)是最務(wù)實(shí)的方案。云存儲(chǔ)的優(yōu)點(diǎn)在于第一和云函數(shù)、云數(shù)據(jù)庫(kù)同屬一套環(huán)境前端直接調(diào)用wx.cloud.uploadFile就能把圖片傳上去返回一個(gè) fileID第二fileID 可以直接在小程序端用image組件的src屬性渲染不需要額外做鑒權(quán)云開(kāi)發(fā)會(huì)自動(dòng)處理第三不用考慮圖片服務(wù)器的帶寬和磁盤(pán)擴(kuò)容問(wèn)題。你唯一要做的是在云開(kāi)發(fā)控制臺(tái)里給云存儲(chǔ)設(shè)置一個(gè)合理的權(quán)限。默認(rèn)權(quán)限建議改成「僅創(chuàng)建者可讀寫(xiě)」因?yàn)槭镎蓄I(lǐng)的圖片會(huì)包含個(gè)人物品信息如果允許所有人讀數(shù)據(jù)庫(kù)里存了 fileID 就等于所有人能看圖片不合適。自建文件服務(wù)器倒也不是不行但你要額外處理跨域CORS、防盜鏈、HTTPS 證書(shū)、圖片壓縮這四件事每一件都能消耗兩三天而且和微信小程序的對(duì)接還得走業(yè)務(wù)域名配置。標(biāo)題給的定位是「源碼 說(shuō)明文檔 演示視頻」說(shuō)明這是一個(gè)以交付為導(dǎo)向的項(xiàng)目云存儲(chǔ)能把交付周期從三周壓到一周。核心權(quán)衡點(diǎn)在表里對(duì)比維度微信云存儲(chǔ)自建 Nginx / FastDFS部署成本零部署開(kāi)通即用需要服務(wù)器配 Nginx 和上傳接口權(quán)限控制控制臺(tái)配置按 openid 粒度需要自己寫(xiě)鑒權(quán)邏輯小程序鑒權(quán)fileID 直接渲染需配域名 HTTPS處理防盜鏈適合場(chǎng)景畢設(shè)、課設(shè)、原型驗(yàn)證有現(xiàn)成服務(wù)器和運(yùn)維能力的場(chǎng)景3. 小程序端三個(gè)必寫(xiě)頁(yè)面發(fā)布頁(yè)、列表頁(yè)、詳情認(rèn)領(lǐng)頁(yè)3.1 發(fā)布頁(yè)圖片上傳的坑與 wx.chooseMedia 參數(shù)配置發(fā)布頁(yè)是用戶接觸系統(tǒng)的第一個(gè)入口也是代碼里最容易出問(wèn)題的頁(yè)面。核心功能就兩個(gè)填物品信息、上傳圖片。圖片上傳的官方接口是wx.chooseMedia它替代了舊版的wx.chooseImage。很多人寫(xiě)到這里只配了count和mediaType就完事結(jié)果真機(jī)上一選相冊(cè)就白屏原因多半是sourceType或sizeType沒(méi)配對(duì)。// pages/publish/publish.js Page({ data: { imageList: [], type: 1, // 默認(rèn)選擇失物招領(lǐng)1招領(lǐng) 2尋物 title: , description: }, // 選擇圖片 chooseImages() { wx.chooseMedia({ count: 3 - this.data.imageList.length, // 最多3張減去已選數(shù)量 mediaType: [image], sourceType: [album, camera], sizeType: [compressed], // 強(qiáng)制壓縮避免原圖過(guò)大上傳失敗 success: (res) { const selected res.tempFiles.map(file file.tempFilePath); this.setData({ imageList: this.data.imageList.concat(selected) }); } }); }, // 上傳圖片到云存儲(chǔ)返回 fileID 數(shù)組 async uploadImages() { const promises this.data.imageList.map((filePath, index) { const cloudPath lost/${Date.now()}-${index}-${Math.random().toString(36).slice(-6)}.jpg; return wx.cloud.uploadFile({ cloudPath, filePath }).then(res res.fileID); }); return Promise.all(promises); } });這段代碼里的sizeType: [compressed]是關(guān)鍵。校園用戶用手機(jī)拍失物照片原圖可能是 3MB 以上的大圖直接上傳到云存儲(chǔ)會(huì)慢、偶爾還會(huì)超時(shí)失敗。壓縮后一般 200KB 以內(nèi)上傳速度可以接受列表頁(yè)渲染也更流暢。cloudPath里拼了時(shí)間戳和隨機(jī)字符串是為了避免同一用戶反復(fù)上傳時(shí)文件名沖突——云存儲(chǔ)的cloudPath如果重復(fù)后傳的文件會(huì)覆蓋先傳的這個(gè)坑我就踩過(guò)。上傳后的 fileID 不要直接放到items集合里要先調(diào)用云函數(shù)寫(xiě)入數(shù)據(jù)庫(kù)。這樣做的原因是前端直接把數(shù)據(jù)寫(xiě)到數(shù)據(jù)庫(kù)繞過(guò)了云函數(shù)的權(quán)限校驗(yàn)用戶可以把status改成任意值等于給自己開(kāi)了后門(mén)。正確姿勢(shì)是前端收集表單數(shù)據(jù)后調(diào)wx.cloud.callFunction({ name: publishItem, data: {...} })由云函數(shù)統(tǒng)一校驗(yàn)入?yún)⒉?xiě)入數(shù)據(jù)庫(kù)。版權(quán)聲明可以不放但代碼里一定要保留入?yún)⑿r?yàn)和openid獲取的邏輯。3.2 列表頁(yè)下拉刷新與觸底分頁(yè)的經(jīng)典寫(xiě)法列表頁(yè)是失物招領(lǐng)系統(tǒng)的門(mén)面它的表現(xiàn)直接決定用戶留不留。技術(shù)上要處理的就兩件事分頁(yè)加載和下拉刷新。很多人第一次寫(xiě)云開(kāi)發(fā)數(shù)據(jù)庫(kù)查詢習(xí)慣直接db.collection(items).get()一把梭結(jié)果數(shù)據(jù)量過(guò)了幾百條就變慢。云開(kāi)發(fā)的默認(rèn)限制是一次最多返回 20 條不是 100 條導(dǎo)致后面數(shù)據(jù)永遠(yuǎn)加載不出來(lái)。正確做法是配合skip和limit實(shí)現(xiàn)分頁(yè)再利用小程序原生的onPullDownRefresh和onReachBottom兩個(gè)生命周期處理刷新和加載更多。// pages/index/index.js Page({ data: { items: [], page: 0, pageSize: 10, hasMore: true, keyword: }, async loadItems(reset false) { if (reset) { this.setData({ page: 0, items: [], hasMore: true }); } if (!this.data.hasMore) return; const db wx.cloud.database(); const { page, pageSize, keyword } this.data; // 關(guān)鍵詞搜索正則模糊匹配標(biāo)題 const where { status: db.command.neq(3) // 排除已下架 }; if (keyword) { where.title db.RegExp({ regexp: keyword, options: i }); } const res await db.collection(items) .where(where) .orderBy(createTime, desc) .skip(page * pageSize) .limit(pageSize) .get(); const newItems reset ? res.data : this.data.items.concat(res.data); this.setData({ items: newItems, page: page 1, hasMore: res.data.length pageSize }); }, onPullDownRefresh() { this.loadItems(true).then(() wx.stopPullDownRefresh()); }, onReachBottom() { this.loadItems(); } });注意page的自增時(shí)機(jī)是在拿到數(shù)據(jù)之后不是請(qǐng)求之前否則失敗重試時(shí)會(huì)跳過(guò)一頁(yè)導(dǎo)致數(shù)據(jù)漏掉。db.RegExp是云開(kāi)發(fā)端提供的正則查詢方式做標(biāo)題模糊搜索夠用但不要用它做description字段的匹配——正則查詢不支持索引數(shù)據(jù)量大時(shí)會(huì)有性能問(wèn)題。status: db.command.neq(3)這個(gè)寫(xiě)法只在items集合上生效如果你用的是 MySQL 后端等價(jià)寫(xiě)法是SELECT ... WHERE status ! 3。這里能看出來(lái)云開(kāi)發(fā)數(shù)據(jù)庫(kù)的查詢 API 學(xué)習(xí)成本不高但一些邊界條件比如neq是db.command下的方法容易記混。下拉刷新還有一個(gè)容易被忽略的點(diǎn)app.json里要開(kāi)啟enablePullDownRefresh: true并且建議在window配置里設(shè)backgroundTextStyle: dark否則 iOS 下拉刷新的轉(zhuǎn)圈是白色的白底上看不見(jiàn)用戶會(huì)以為刷新功能壞了。3.3 詳情頁(yè)與認(rèn)領(lǐng)流程用狀態(tài)機(jī)驅(qū)動(dòng)按鈕渲染詳情頁(yè)是認(rèn)領(lǐng)流程的核心載體。一張物品卡片上有發(fā)布者信息、物品描述、圖片、聯(lián)系方式和操作按鈕。按鈕怎么渲染不能寫(xiě)死得根據(jù)當(dāng)前用戶和物品狀態(tài)動(dòng)態(tài)計(jì)算。比如物品是自己的顯示“刪除”和“標(biāo)記已完成”物品是別人的且狀態(tài)為待處理顯示“申請(qǐng)認(rèn)領(lǐng)”已經(jīng)提交過(guò)認(rèn)領(lǐng)申請(qǐng)顯示“審核中”。這種多分支渲染最清晰的做法是先梳理狀態(tài)矩陣再寫(xiě)wx:if。// pages/detail/detail.js Page({ data: { item: null, claimStatus: -1, // -1 未申請(qǐng)0 待審核1 已通過(guò)2 已拒絕 isOwner: false }, // 計(jì)算按鈕狀態(tài) buildActions() { const { item, claimStatus, isOwner } this.data; const actions []; if (isOwner) { if (item.status 0 || item.status 1) { actions.push({ key: finish, text: 標(biāo)記已完成 }); } if (item.status ! 2) { actions.push({ key: remove, text: 下架物品 }); } } else if (item.status 0) { if (claimStatus -1) { actions.push({ key: claim, text: 申請(qǐng)認(rèn)領(lǐng) }); } else if (claimStatus 0) { actions.push({ key: pending, text: 審核中, disabled: true }); } else if (claimStatus 1) { actions.push({ key: contact, text: 聯(lián)系發(fā)布者 }); } else { actions.push({ key: claim, text: 重新申請(qǐng) }); } } this.setData({ actions }); } });claimStatus是查claims集合得到的當(dāng)前用戶對(duì)這個(gè)物品的申請(qǐng)狀態(tài)。這里有一個(gè)先后順序問(wèn)題必須等item和claimStatus都拿到后再渲染按鈕否則用戶會(huì)看到按鈕閃爍一下體驗(yàn)很糟。常見(jiàn)的做法是在onLoad里并行發(fā)起兩個(gè)請(qǐng)求用Promise.all聚合后再調(diào)buildActions()。認(rèn)領(lǐng)申請(qǐng)的動(dòng)作要在云函數(shù)里完成前端不能直接寫(xiě)claims集合。云函數(shù)至少要做三件事檢查物品狀態(tài)是否為 0防止已完成的物品被重復(fù)申請(qǐng)檢查該用戶是否已申請(qǐng)過(guò)防止重復(fù)提交寫(xiě)入認(rèn)領(lǐng)記錄并將物品狀態(tài)改為 1待審核。這三步在云函數(shù)里按順序執(zhí)行即使前端被用戶反復(fù)點(diǎn)擊也能通過(guò)數(shù)據(jù)庫(kù)的狀態(tài)約束擋住。4. 后端接口與核心邏輯云函數(shù)怎么寫(xiě)才不返工4.1 云函數(shù)和自建后端的取舍數(shù)據(jù)權(quán)限與調(diào)用成本微信云開(kāi)發(fā)模式下后端邏輯主要寫(xiě)在云函數(shù)里。一個(gè)云函數(shù)對(duì)應(yīng)一個(gè)exports.main入口相當(dāng)于一個(gè) HTTP 接口。和傳統(tǒng)自建后端比云函數(shù)最大的特點(diǎn)是不需要關(guān)心服務(wù)器但代價(jià)是冷啟動(dòng)延遲和調(diào)用次數(shù)限制。對(duì)失物招領(lǐng)這種低頻業(yè)務(wù)冷啟動(dòng)幾乎無(wú)感免費(fèi)額度也足夠。真正影響選型的是數(shù)據(jù)權(quán)限。云開(kāi)發(fā)數(shù)據(jù)庫(kù)有三種權(quán)限設(shè)置僅創(chuàng)建者可讀寫(xiě)、所有用戶可讀僅創(chuàng)建者可寫(xiě)、所有用戶可讀寫(xiě)。失物招領(lǐng)系統(tǒng)里items集合建議用「所有用戶可讀僅創(chuàng)建者可寫(xiě)」。因?yàn)榱斜眄?yè)和詳情頁(yè)的查詢是高頻操作如果都走云函數(shù)每次都調(diào)用一次云函數(shù)不僅慢還會(huì)消耗云函數(shù)調(diào)用次數(shù)。但claims集合必須收緊權(quán)限認(rèn)領(lǐng)記錄屬于敏感數(shù)據(jù)不能讓所有用戶讀。你需要保證“讀公開(kāi)數(shù)據(jù)走數(shù)據(jù)庫(kù)直連寫(xiě)數(shù)據(jù)走云函數(shù)校驗(yàn)”。這是一個(gè)混合策略也是大多數(shù)線上小程序的實(shí)際寫(xiě)法。自建后端則是把所有讀寫(xiě)都收口到自己的接口。優(yōu)點(diǎn)是邏輯看得見(jiàn)摸得著調(diào)試手段更豐富缺點(diǎn)是要處理登錄態(tài)校驗(yàn)、用戶身份識(shí)別。微信小程序要自建后端通常流程是wx.login拿 code后端用 code 換 openid然后簽發(fā)自定義 token。這套流程在畢業(yè)設(shè)計(jì)里也能做但涉及 HTTPS 證書(shū)、code2session 接口調(diào)用工作量直接翻倍。如果不是為了練習(xí) Java 或 Node 后端云函數(shù)是更劃算的選擇。4.2 關(guān)鍵詞匹配與模糊查找讓用戶快速找到物品的查詢寫(xiě)法失物招領(lǐng)信息一多光靠列表滾動(dòng)沒(méi)法用必須做搜索。搜索的核心是關(guān)鍵詞匹配用戶輸入的物品名稱。微信云開(kāi)發(fā)數(shù)據(jù)庫(kù)支持db.RegExp可以用正則表達(dá)式做模糊匹配。但要注意正則匹配在數(shù)據(jù)量超過(guò)幾千條后性能會(huì)明顯下降所以搜索條件里要配合其他篩選條件一起用比如加上status過(guò)濾和category分類(lèi)。下面這個(gè)云函數(shù)實(shí)現(xiàn)了掃碼式搜索接收keyword先按標(biāo)題模糊匹配標(biāo)題匹配不到再用description做全文匹配。兩個(gè)查詢并行發(fā)起結(jié)果合并去重后返回。去重邏輯用itemId做 map 的鍵一勞永逸避免重復(fù)。// cloudfunctions/searchItems/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; exports.main async (event) { const { keyword , category , page 0, pageSize 10 } event; const reg db.RegExp({ regexp: keyword, options: i }); try { // 并行查標(biāo)題和描述減少響應(yīng)時(shí)間 const [titleRes, descRes] await Promise.all([ db.collection(items) .where({ title: reg, status: _.neq(3), ...(category ? { category } : {}) }) .limit(pageSize) .get(), db.collection(items) .where({ description: reg, status: _.neq(3), ...(category ? { category } : {}) }) .limit(pageSize) .get() ]); // 合并去重 const map new Map(); [...titleRes.data, ...descRes.data].forEach(item { if (!map.has(item._id)) { map.set(item._id, { ...item, matchField: item.title.match(reg) ? title : description }); } }); return { ok: true, data: Array.from(map.values()).slice(0, pageSize), count: map.size }; } catch (err) { return { ok: false, message: err.message }; } };Promise.all在這里的意義不是炫技而是把兩次串行查詢變成并行省一半時(shí)間。matchField字段用于前端展示時(shí)高亮匹配位置雖然高亮不是必須的但說(shuō)明文檔里寫(xiě)清楚“搜索結(jié)果可識(shí)別匹配字段”答辯時(shí)能體現(xiàn)設(shè)計(jì)的完整性。db.RegExp的對(duì)象不能直接展開(kāi)到擴(kuò)展運(yùn)算符里必須嵌套在where條件內(nèi)否則查詢不會(huì)生效。這一點(diǎn)卡過(guò)不少人。4.3 認(rèn)領(lǐng)核銷(xiāo)與防冒領(lǐng)用狀態(tài)流轉(zhuǎn)和操作日志兜底認(rèn)領(lǐng)流程是失物招領(lǐng)系統(tǒng)的核心也是最容易被簡(jiǎn)化處理的部分。簡(jiǎn)化版的實(shí)現(xiàn)是用戶在詳情頁(yè)點(diǎn)“認(rèn)領(lǐng)”把items.status改成 2已完成然后完事。這個(gè)流程在演示視頻里看起來(lái)很流暢但實(shí)際使用時(shí)任何人都能點(diǎn)一下就把別人的東西“領(lǐng)走”冒領(lǐng)問(wèn)題無(wú)法避免。我在實(shí)際項(xiàng)目中用的方案是“描述核對(duì) 管理員審核”雙保險(xiǎn)。用戶在詳情頁(yè)填一段描述說(shuō)明物品的特征細(xì)節(jié)比如校園卡上的姓名拼音、錢(qián)包的顏色和品牌。云函數(shù)收到后把這條記錄寫(xiě)入claims集合同時(shí)把items.status改為 1待審核。發(fā)布者在“我的發(fā)布”里看到待審核狀態(tài)后能查看認(rèn)領(lǐng)者填寫(xiě)的描述對(duì)照自己的物品特征決定通過(guò)或拒絕。如果發(fā)布者沒(méi)空處理管理員也能在管理端介入。這套流程用云函數(shù)實(shí)現(xiàn)時(shí)關(guān)鍵在于對(duì)并發(fā)提交的防護(hù)。用戶連續(xù)點(diǎn)擊兩次“申請(qǐng)認(rèn)領(lǐng)”可能產(chǎn)生兩條認(rèn)領(lǐng)記錄導(dǎo)致物品狀態(tài)錯(cuò)亂。解決方法是云函數(shù)內(nèi)部用事務(wù)。云開(kāi)發(fā)支持db.runTransaction是處理分布式事務(wù)的推薦方式能在并發(fā)場(chǎng)景下保證數(shù)據(jù)一致性。下面是我常用的原子化認(rèn)領(lǐng)寫(xiě)法// cloudfunctions/claimItem/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); exports.main async (event) { const { OPENID } cloud.getWXContext(); const { itemId, description } event; try { return await db.runTransaction(async transaction { const item await transaction.collection(items).doc(itemId).get(); // 雙重校驗(yàn)物品必須存在且狀態(tài)為 0 if (!item.data || item.data.status ! 0) { return { ok: false, message: 物品不存在或已被認(rèn)領(lǐng) }; } // 校驗(yàn)用戶是否重復(fù)申請(qǐng) const claim await transaction.collection(claims) .where({ itemId, openid: OPENID, status: _.neq(2) }) .get(); if (claim.data.length 0) { return { ok: false, message: 請(qǐng)勿重復(fù)申請(qǐng) }; } // 寫(xiě)入認(rèn)領(lǐng)記錄并更新物品狀態(tài) await transaction.collection(claims).add({ data: { itemId, openid: OPENID, description: description || , status: 0, createTime: db.serverDate() } }); await transaction.collection(items).doc(itemId).update({ data: { status: 1 } }); return { ok: true, message: 認(rèn)領(lǐng)申請(qǐng)?zhí)峤怀晒?}; }); } catch (err) { return { ok: false, message: err.message }; } };runTransaction的寫(xiě)法和普通云函數(shù)明顯不同它把所有操作放在transaction對(duì)象上執(zhí)行。如果中途任一步出錯(cuò)事務(wù)會(huì)自動(dòng)回滾不會(huì)出現(xiàn)“認(rèn)領(lǐng)記錄寫(xiě)入了但物品狀態(tài)還是待認(rèn)領(lǐng)”這種臟數(shù)據(jù)。事務(wù)內(nèi)的get是讀取當(dāng)前版本的數(shù)據(jù)能避免兩個(gè)人同時(shí)提交時(shí)都通過(guò)了status ! 0的判斷。這段代碼是說(shuō)明文檔里最好的素材也是答辯時(shí)“系統(tǒng)設(shè)計(jì)考慮了并發(fā)安全”的有力證據(jù)。5. 避坑合集圖片、緩存、狀態(tài)不同步的五個(gè)經(jīng)典踩坑記錄5.1 圖片上傳失敗錯(cuò)誤碼卻是 success現(xiàn)象wx.chooseMedia回調(diào)顯示成功uploadFile也返回了fileID但詳情頁(yè)圖片加載不出來(lái)控制臺(tái)報(bào) 404。這個(gè)問(wèn)題的隱蔽之處在于失敗不是上傳環(huán)節(jié)而是cloudPath包含了中文或特殊字符。某些版本的云存儲(chǔ)對(duì)中文路徑處理不友好會(huì)靜默失敗或返回一個(gè)無(wú)法訪問(wèn)的 fileID。解決cloudPath只用字母、數(shù)字、橫杠和點(diǎn)。我建議用time random ext的格式擴(kuò)展名從filePath里取不要寫(xiě)死.jpg因?yàn)橛脩艨赡芘牡氖?png 或 heic 格式。另外注意wx.uploadFile一次只能傳一個(gè)文件如果選擇多圖必須用Promise.all并發(fā)上傳但并發(fā)數(shù)不要超過(guò) 3。超過(guò) 3 個(gè)文件同時(shí)上傳部分手機(jī)在弱網(wǎng)環(huán)境下會(huì)超時(shí)表現(xiàn)是“上傳了但列表里少圖”。處理辦法是失敗重試重試次數(shù)控制在 2 次以內(nèi)同時(shí)給用戶提示“圖片上傳失敗請(qǐng)檢查網(wǎng)絡(luò)”。5.2 列表頁(yè)滑動(dòng)快了會(huì)跳數(shù)據(jù)現(xiàn)象快速下拉或連續(xù)觸底時(shí)頁(yè)面出現(xiàn)重復(fù)的卡片或者幾個(gè)列表項(xiàng)消失。原因分頁(yè)用的是page計(jì)數(shù)但每次觸底時(shí)page的遞增發(fā)生在異步請(qǐng)求返回之后用戶快速滾動(dòng)時(shí)會(huì)發(fā)出多次請(qǐng)求多個(gè)請(qǐng)求的skip參數(shù)相同自然返回同一批數(shù)據(jù)。解決在data里加一個(gè)isLoading鎖請(qǐng)求開(kāi)始時(shí)置為true請(qǐng)求結(jié)束后置為falseonReachBottom里先判斷鎖鎖開(kāi)著就直接return。這是列表分頁(yè)的常備武器任何小程序項(xiàng)目都適用。5.3 認(rèn)領(lǐng)狀態(tài)前端一直不刷新現(xiàn)象用戶在詳情頁(yè)提交認(rèn)領(lǐng)返回后再次進(jìn)入詳情頁(yè)按鈕還是“申請(qǐng)認(rèn)領(lǐng)”沒(méi)有變成“審核中”。原因詳情頁(yè)的onLoad只會(huì)在頁(yè)面首次加載時(shí)執(zhí)行從詳情頁(yè)返回列表頁(yè)再進(jìn)入頁(yè)面實(shí)例被緩存onLoad不再觸發(fā)。這是小程序經(jīng)典的“頁(yè)面棧緩存”問(wèn)題。解決把狀態(tài)查詢邏輯從onLoad挪到onShow中這樣每次頁(yè)面顯示時(shí)都會(huì)重新拉取claimStatus。代價(jià)是多一次請(qǐng)求但對(duì)低頻的詳情頁(yè)來(lái)說(shuō)完全可以接受。5.4 演示視頻里最容易翻車(chē)的初始化數(shù)據(jù)太隨意現(xiàn)象演示視頻錄到一半發(fā)現(xiàn)列表頁(yè)沒(méi)有數(shù)據(jù)或者在“認(rèn)領(lǐng)”環(huán)節(jié)查不到已經(jīng)發(fā)布的物品。這個(gè)翻車(chē)多數(shù)不是程序 bug而是演示前沒(méi)有準(zhǔn)備演示數(shù)據(jù)。數(shù)據(jù)庫(kù)里空蕩蕩功能再完整也顯得空洞。解決寫(xiě)一個(gè)初始化云函數(shù)一次性插入 1015 條模擬數(shù)據(jù)覆蓋不同狀態(tài)待處理、待審核、已完成和不同分類(lèi)。模擬數(shù)據(jù)的createTime要錯(cuò)開(kāi)比如最近一小時(shí)幾條、最近一天幾條、最近一周幾條這樣列表頁(yè)下拉刷新和分頁(yè)都有內(nèi)容可展示。視頻錄制前先跑一遍初始化腳本比現(xiàn)場(chǎng)造數(shù)據(jù)穩(wěn)妥得多。5.5 管理員審核并發(fā)點(diǎn)擊狀態(tài)記錄錯(cuò)亂現(xiàn)象管理端同時(shí)打開(kāi)多個(gè)詳情頁(yè)對(duì)同一條物品的認(rèn)領(lǐng)申請(qǐng)連續(xù)做“通過(guò)”操作最后物品狀態(tài)變成了“已完成”但認(rèn)領(lǐng)記錄卻有兩條通過(guò)。原因管理端的審核邏輯如果寫(xiě)在頁(yè)面里直接調(diào)數(shù)據(jù)庫(kù)就沒(méi)有事務(wù)保護(hù)的。并發(fā)環(huán)境下兩個(gè)請(qǐng)求都讀到了“待審核”狀態(tài)都執(zhí)行了通過(guò)操作。解決參考 4.3 的事務(wù)寫(xiě)法把審核動(dòng)作也收口到云函數(shù)。審核云函數(shù)里用transaction讀取認(rèn)領(lǐng)記錄和物品狀態(tài)先判斷claim.status 0再更新從根上杜絕重復(fù)審核。管理后臺(tái)的代碼量不大但這一步不能省。6. 演示視頻與說(shuō)明文檔的落地技巧讓交付物真正為你加分源碼、說(shuō)明文檔、演示視頻三件套里演示視頻是最容易被忽視的。很多人臨近答辯才拿手機(jī)對(duì)著屏幕隨手一錄開(kāi)頭卡頓、中間翻車(chē)、結(jié)尾黑屏功能再全也抵不過(guò)觀感差。我的習(xí)慣是先寫(xiě)視頻腳本再錄內(nèi)容最后做剪輯。腳本按功能模塊劃分每個(gè)模塊控制在 3045 秒全片不超過(guò) 5 分鐘。開(kāi)場(chǎng) 10 秒展示小程序首頁(yè)列表和下拉刷新接著演示發(fā)布流程上傳圖片和填寫(xiě)表單然后是搜索功能輸入一個(gè)關(guān)鍵詞看結(jié)果高亮第三段演示認(rèn)領(lǐng)閉環(huán)——提交申請(qǐng)、管理員審核、狀態(tài)變?yōu)橐淹瓿伞C恳欢吻凹右粋€(gè)字幕條說(shuō)明當(dāng)前演示的功能點(diǎn)觀看者不用猜你在干什么。錄制時(shí)注意三個(gè)階段的數(shù)據(jù)準(zhǔn)備第一階段列表有 5 條未處理的信息第二階段新增一條尋物圖片清晰第三階段用兩個(gè)微信號(hào)配合一個(gè)發(fā)招領(lǐng)另一個(gè)申請(qǐng)認(rèn)領(lǐng)模擬真實(shí)用戶互動(dòng)。如果只有一個(gè)微信號(hào)就通過(guò)云開(kāi)發(fā)控制臺(tái)直接修改數(shù)據(jù)庫(kù)里claims的status字段來(lái)展示審核效果管理員審核頁(yè)面的操作仍然走真實(shí)界面。這個(gè)“半真半假”的錄法在演示視頻里是常見(jiàn)做法功能是真的只是狀態(tài)靠后臺(tái)配合。說(shuō)明文檔的重點(diǎn)放在系統(tǒng)架構(gòu)圖、數(shù)據(jù)庫(kù)設(shè)計(jì)、核心流程時(shí)序圖這三塊不用追求大而全的八股文。架構(gòu)圖里把小程序端、云函數(shù)、云數(shù)據(jù)庫(kù)、云存儲(chǔ)四層畫(huà)清楚標(biāo)注每個(gè)請(qǐng)求的路徑前端通過(guò)wx.cloud.callFunction調(diào)用云函數(shù)云函數(shù)操作數(shù)據(jù)庫(kù)和存儲(chǔ)。數(shù)據(jù)庫(kù)設(shè)計(jì)部分放表格列出每張集合的字段、類(lèi)型、含義。核心流程時(shí)序圖畫(huà)“發(fā)布→搜索→認(rèn)領(lǐng)→審核→核銷(xiāo)”的完整鏈路一個(gè)圖頂過(guò)千字描述。文檔寫(xiě)完以后把 3.3 的狀態(tài)矩陣和 4.3 的事務(wù)代碼單獨(dú)摘出來(lái)放在附錄里這兩個(gè)細(xì)節(jié)能給文檔的深度拉高一個(gè)檔次。開(kāi)源或交付源碼時(shí)記得清理app.js里你的env環(huán)境 ID替換成占位符your-env-id并告訴接收者在哪里配置。很多源碼交付出去跑不起來(lái)九成是環(huán)境 ID 還是原作者的用戶自己的云環(huán)境根本沒(méi)有那個(gè) ID 對(duì)應(yīng)的集合。把初始化腳本、云函數(shù)目錄結(jié)構(gòu)、環(huán)境配置三步寫(xiě)進(jìn) README 的第一屏比寫(xiě)什么“項(xiàng)目介紹”都實(shí)用。這也是我交付過(guò)幾套源碼后的血淚經(jīng)驗(yàn)代碼能不能跑往往不取決于代碼本身而取決于接收者能否在五分鐘內(nèi)完成環(huán)境配置。希望這些經(jīng)驗(yàn)?zāi)軒湍阍诮桓逗痛疝q時(shí)少走彎路祝你順利跑通整套流程。本文還有配套的精品資源點(diǎn)擊獲取