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