約系統(tǒng)源碼:并發(fā)控制與微信登錄避坑實(shí)踐)
簡(jiǎn)介面向計(jì)算機(jī)、電子信息工程及數(shù)學(xué)等專業(yè)的畢業(yè)設(shè)計(jì)學(xué)生這是一份微信小程序駕校預(yù)約管理系統(tǒng)的完整源碼基于Java技術(shù)棧實(shí)現(xiàn)經(jīng)導(dǎo)師指導(dǎo)評(píng)定為98分的優(yōu)秀畢設(shè)項(xiàng)目。系統(tǒng)涵蓋用戶端小程序與管理后臺(tái)支持駕校約車、排課、學(xué)員信息管理、訂單追蹤等核心功能幫助開(kāi)發(fā)者理解前后端分離的小程序完整業(yè)務(wù)閉環(huán)適合用于畢業(yè)設(shè)計(jì)參考、課程設(shè)計(jì)及項(xiàng)目實(shí)戰(zhàn)練習(xí)。資源包共1207個(gè)文件壓縮后大小13.2MB內(nèi)部包含Java后端邏輯96個(gè)java文件、Vue后臺(tái)管理頁(yè)面122個(gè)vue文件、微信小程序前端js/wxml/wxss等以及png/svg界面素材、json配置、SQL數(shù)據(jù)庫(kù)腳本與項(xiàng)目說(shuō)明文檔。文件類型覆蓋開(kāi)發(fā)、配置、靜態(tài)資源與腳本目錄結(jié)構(gòu)按模塊劃分清晰并提供了1-install、2-run、3-build等批處理腳本方便快速啟動(dòng)項(xiàng)目。目前已有202人學(xué)習(xí)下載代碼經(jīng)嚴(yán)格調(diào)試無(wú)明顯Bug既有完整業(yè)務(wù)實(shí)現(xiàn)又保留擴(kuò)展空間可直接作為畢設(shè)交付或在此基礎(chǔ)上繼續(xù)二次開(kāi)發(fā)。1. 駕校預(yù)約管理系統(tǒng)源碼拿到手先別急著跑先認(rèn)清它的業(yè)務(wù)骨架“微信小程序的駕校預(yù)約管理系統(tǒng)源碼”這幾年在畢設(shè)選題里出現(xiàn)頻率極高搜一下滿屏都是“高分畢設(shè)”“可直接運(yùn)行”的后綴。但把源碼導(dǎo)入微信開(kāi)發(fā)者工具之后白屏、接口 404、手機(jī)號(hào)解不出來(lái)、同一時(shí)段被兩個(gè)人同時(shí)約上才是大多數(shù)人真正遇到的第一課。這套系統(tǒng)說(shuō)白了就是把“學(xué)員選教練、選時(shí)間段、提交預(yù)約、后臺(tái)審核與統(tǒng)計(jì)”這條鏈路搬到小程序上前端用微信小程序承載后端配一套管理接口和數(shù)據(jù)庫(kù)。適合正在做畢設(shè)或課程設(shè)計(jì)的人也適合想接駕校類私活但缺一個(gè)干凈骨架的開(kāi)發(fā)者。下面按我從零搭過(guò)、也幫人救過(guò)火的實(shí)際路徑來(lái)講重點(diǎn)放在“能復(fù)現(xiàn)”和“敢演示”上。2. 系統(tǒng)拆解與選型三類角色、四張核心表和一條狀態(tài)機(jī)2.1 角色邊界學(xué)員、教練、管理員各自看到什么駕校預(yù)約管理系統(tǒng)的第一件事不是寫(xiě)代碼而是分清角色。學(xué)員端看到的是“我能約哪個(gè)教練、哪個(gè)時(shí)段還有空位、我約過(guò)的記錄長(zhǎng)什么樣”教練端看到的是“我今天的排班、誰(shuí)約了我、誰(shuí)沒(méi)來(lái)”管理員端看到的則是“教練怎么排、時(shí)段怎么配、預(yù)約怎么審、數(shù)據(jù)怎么統(tǒng)計(jì)”。三種角色如果混在一張頁(yè)面里后面每次改需求都會(huì)變成災(zāi)難。常見(jiàn)的項(xiàng)目結(jié)構(gòu)是學(xué)員用微信授權(quán)登錄教練和管理員在同一個(gè)后臺(tái)里按角色切換菜單。實(shí)現(xiàn)上不需要太重的權(quán)限框架user 表里一個(gè) role 字段0 學(xué)員、1 教練、2 管理員接口層根據(jù)登錄態(tài)里的 role 做過(guò)濾就行。答辯時(shí)被問(wèn)“多角色權(quán)限怎么實(shí)現(xiàn)”你要答的不是 RBAC 多表而是“按角色控制接口可見(jiàn)性和數(shù)據(jù)范圍”這句話一聽(tīng)就是做過(guò)的人。2.2 四張核心表和字段設(shè)計(jì)預(yù)約不重不漏的關(guān)鍵駕校預(yù)約看起來(lái)功能多落到數(shù)據(jù)庫(kù)其實(shí)就四張核心表用戶表、教練表、時(shí)段表、預(yù)約表。用戶表存 openid 和手機(jī)號(hào)教練表存姓名、所屬駕校、科目類型、狀態(tài)時(shí)段表是預(yù)約系統(tǒng)的靈魂存日期、教練、開(kāi)始時(shí)間、結(jié)束時(shí)間、容量、已約數(shù)量預(yù)約表存誰(shuí)在哪個(gè)時(shí)段約了哪位教練。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE, phone VARCHAR(20) DEFAULT , nickname VARCHAR(64) DEFAULT , role TINYINT NOT NULL DEFAULT 0 ); CREATE TABLE coach ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, school VARCHAR(128) DEFAULT , subject TINYINT DEFAULT 3, status TINYINT DEFAULT 1 ); CREATE TABLE period ( id INT PRIMARY KEY AUTO_INCREMENT, coach_id INT NOT NULL, date DATE NOT NULL, start_time VARCHAR(5) NOT NULL, end_time VARCHAR(5) NOT NULL, capacity INT DEFAULT 1, booked INT DEFAULT 0, UNIQUE KEY uk_coach_date_time (coach_id, date, start_time) ); CREATE TABLE booking ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, period_id INT NOT NULL, status VARCHAR(16) DEFAULT pending, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_period (user_id, period_id) );這里有兩個(gè)容易被忽略的設(shè)計(jì)。第一period 表必須加唯一索引防止同一個(gè)教練在同一個(gè)日期的同一個(gè)開(kāi)始時(shí)間被插入兩條時(shí)段記錄。第二booking 表對(duì)“同一用戶同一時(shí)段”加唯一索引是防重復(fù)預(yù)約的最后一道防線。很多畢設(shè)源碼只靠前端按鈕禁用擋重復(fù)提交后端沒(méi)有唯一索引這是典型的“演示沒(méi)問(wèn)題、一上真機(jī)就翻車”的隱患。2.3 為什么選微信小程序這套姿態(tài)原生還是 uni-app、后端怎么配選型理由要能說(shuō)給答辯老師聽(tīng)。小程序端的核心優(yōu)勢(shì)是免安裝演示時(shí)一臺(tái)手機(jī)掃碼就能跑比裝一個(gè)安卓 APK 體面得多。前端技術(shù)棧上標(biāo)題寫(xiě)的是“微信小程序”源碼可能是原生小程序也可能是 uni-app 編譯產(chǎn)物。拿到代碼先看根目錄有沒(méi)有 pages.json 和 manifest.json有 pages.json 的是 uni-app有 app.json 的是原生小程序。原生小程序適合畢設(shè)因?yàn)槲⑿砰_(kāi)發(fā)者工具直接打開(kāi)就能跑uni-app 的好處是以后想發(fā)布到支付寶小程序或 App 不用重寫(xiě)但代價(jià)是調(diào)試時(shí)要多一層編譯。后端的選擇比較雜。老一點(diǎn)的源碼用 PHP新一點(diǎn)的用 Spring Boot也有 Node.js 和 Python Flask 寫(xiě)的。我的建議是別迷信“哪個(gè)語(yǔ)言拿分高”先看你本地環(huán)境能不能把后端跑起來(lái)。PHP 項(xiàng)目用 phpStudy 起一個(gè) Apache 環(huán)境改一下數(shù)據(jù)庫(kù)連接配置Spring Boot 項(xiàng)目要裝 JDK 和 Maven首次拉依賴可能等很久Flask 項(xiàng)目最輕裝幾個(gè) pip 包就能跑。后端語(yǔ)言不影響業(yè)務(wù)邏輯真正影響你進(jìn)度的只有兩點(diǎn)數(shù)據(jù)庫(kù)賬號(hào)密碼對(duì)不對(duì)以及接口路徑和小程序里配置的 baseURL 是否一致。3. 小程序端跑通最小流程手機(jī)號(hào)登錄、選時(shí)段、提交預(yù)約3.1 用 getPhoneNumber 換手機(jī)號(hào)兩種接法及參數(shù)差異微信小程序的駕校預(yù)約管理系統(tǒng)里登錄是第一步?,F(xiàn)在的接口規(guī)范已經(jīng)調(diào)整過(guò)點(diǎn)擊“手機(jī)號(hào)快捷登錄”按鈕拿到的不再是 encryptedData 和 iv而是一個(gè)動(dòng)態(tài) code需要交給后端去微信接口換手機(jī)號(hào)。先看前端頁(yè)面怎么寫(xiě)。button classlogin-btn open-typegetPhoneNumber bindgetphonenumberhandlePhoneLogin 微信手機(jī)號(hào)快捷登錄 /buttonasync handlePhoneLogin(e) { // 用戶點(diǎn)取消時(shí) errMsg 里帶 fail這里直接攔掉 if (e.detail.errMsg.indexOf(ok) 0) { wx.showToast({ title: 需要授權(quán)手機(jī)號(hào)才能預(yù)約, icon: none }); return; } const phoneCode e.detail.code; // 動(dòng)態(tài)令牌5 分鐘內(nèi)有效 wx.request({ url: ${config.baseURL}/api/login, method: POST, data: { code: phoneCode }, success: (res) { if (res.data.code 0) { wx.setStorageSync(token, res.data.data.token); this.setData({ logged: true, phone: res.data.data.phone }); } } }); }這段代碼有兩個(gè)關(guān)鍵參數(shù)。e.detail.code 是前端能拿到的唯一憑證它不能直接解出手機(jī)號(hào)必須由后端拿著 code 去微信服務(wù)端換取手機(jī)號(hào)token 是后端自定義簽發(fā)的登錄態(tài)后續(xù)所有預(yù)約請(qǐng)求都帶它。很多人在這里犯的錯(cuò)是直接把 code 當(dāng) token 用或者前端自己拿 encryptedData 解密這兩條路都走不通。另外開(kāi)發(fā)者工具里這個(gè)按鈕返回的是模擬數(shù)據(jù)真機(jī)才能走通完整鏈路后面避坑部分會(huì)細(xì)說(shuō)。3.2 時(shí)段卡片渲染與單選狀態(tài)管理幾分鐘搞定可交互預(yù)約頁(yè)預(yù)約頁(yè)的核心交互是“選教練 → 選日期 → 選時(shí)段”。時(shí)段列表從后端接口拉回來(lái)前端渲染成卡片點(diǎn)擊后高亮當(dāng)前選中的那一張。這里要注意小程序里不能用 DOM 操作所有選中態(tài)都要靠 data 驅(qū)動(dòng)。data: { date: , slotList: [], selectedSlotId: null, loading: false }, async loadSlots() { this.setData({ loading: true }); wx.request({ url: ${config.baseURL}/api/slots?date${this.data.date}, method: GET, success: (res) { // 后端返回的是可約時(shí)段已經(jīng)過(guò)濾掉約滿和過(guò)期的 this.setData({ slotList: res.data.data }); }, complete: () this.setData({ loading: false }) }); }, onSelectSlot(e) { const id e.currentTarget.dataset.id; // 點(diǎn)擊同一張卡片時(shí)取消選中符合單選習(xí)慣 if (this.data.selectedSlotId id) { this.setData({ selectedSlotId: null }); return; } this.setData({ selectedSlotId: id }); }渲染層用 wx:for 循環(huán)卡片class 里根據(jù) selectedSlotId 動(dòng)態(tài)切換高亮樣式。這里必須注意 dataset 取出來(lái)的值是字符串和后端返回的數(shù)字 id 比較時(shí)要用 或者手動(dòng)轉(zhuǎn)類型否則會(huì)出現(xiàn)“明明點(diǎn)了卻不高亮”的玄學(xué)問(wèn)題。另一個(gè)細(xì)節(jié)是 complete 里復(fù)位 loading而不是 success這樣接口報(bào)錯(cuò)時(shí)按鈕也能恢復(fù)正常避免頁(yè)面卡死在加載態(tài)。3.3 提交訂單的 wx.request 封裝與防重復(fù)提交預(yù)約提交接口本身不復(fù)雜復(fù)雜的是用戶手快連點(diǎn)兩下。前端如果不做防重就會(huì)出現(xiàn)兩條相同請(qǐng)求打到后端哪怕后端有唯一索引兜底返回給用戶的也是不友好的報(bào)錯(cuò)。所以我習(xí)慣在頁(yè)面里加一個(gè) submitting 鎖。submitBooking() { if (this.data.submitting) return; if (!this.data.selectedSlotId) { wx.showToast({ title: 請(qǐng)先選擇時(shí)段, icon: none }); return; } this.setData({ submitting: true }); wx.request({ url: ${config.baseURL}/api/booking, method: POST, data: { periodId: this.data.selectedSlotId, date: this.data.date }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 預(yù)約成功, icon: success }); this.loadSlots(); // 刷新剩余名額 } else { wx.showToast({ title: res.data.msg, icon: none }); } }, complete: () this.setData({ submitting: false }) }); }submitting 的作用是讓首次點(diǎn)擊后立即把按鈕鎖住直到請(qǐng)求結(jié)束無(wú)論成功失敗才解鎖。如果你的源碼里把解鎖放在 success 回調(diào)里一旦網(wǎng)絡(luò)超時(shí)按鈕會(huì)一直鎖死用戶只能殺掉小程序重進(jìn)。這一點(diǎn)是校驗(yàn)一份源碼會(huì)不會(huì)寫(xiě)前端狀態(tài)控制的好抓手。至于 token 怎么帶一般是在 wx.request 的 header 里加 Authorization 字段遇到 401 狀態(tài)碼時(shí)統(tǒng)一跳到登錄頁(yè)。4. 后端預(yù)約接口沖突校驗(yàn)、狀態(tài)流轉(zhuǎn)和看不見(jiàn)的管理后臺(tái)4.1 建表與預(yù)置數(shù)據(jù)把教練、時(shí)段、訂單落到 MySQL后端是預(yù)約系統(tǒng)真正較勁的地方。前端做再多動(dòng)畫(huà)和樣式接口在處理“同一時(shí)段兩個(gè)人同時(shí)預(yù)約”時(shí)如果設(shè)計(jì)得糙演示現(xiàn)場(chǎng)就會(huì)翻車。常見(jiàn)的畢設(shè)源碼后端有 PHP 和 Java 兩種但業(yè)務(wù)邏輯是同一套我這里用最直觀的 Flask 寫(xiě)法演示你遷移到 Spring Boot 時(shí)核心 SQL 不用變。建完表之后要先造一批可用的預(yù)置數(shù)據(jù)否則小程序端連時(shí)段都拉不到。教練表里至少塞三四個(gè)教練時(shí)段表按“每個(gè)教練每天 6 個(gè)時(shí)段、每個(gè)時(shí)段 1 個(gè)名額”批量生成。預(yù)約功能跑通之前先確認(rèn)一條 SQL 能查出“某天某教練下還有哪些可約時(shí)段”。SELECT p.id, p.start_time, p.end_time, (p.capacity - p.booked) AS remain FROM period p WHERE p.coach_id 1 AND p.date 2025-06-10 AND p.booked p.capacity AND p.start_time NOW() ORDER BY p.start_time;這里的 remain 字段是給前端展示用的真正判斷是否能約不能靠前端傳過(guò)來(lái)的 remain而是要靠后端在寫(xiě)入時(shí)重新校驗(yàn)。很多源碼把“是否約滿”的判斷寫(xiě)在前端后端只負(fù)責(zé)插一條記錄這就是并發(fā)超約的根源。4.2 預(yù)約寫(xiě)接口條件 UPDATE 與事務(wù)干掉并發(fā)超約后端預(yù)約接口的正確姿勢(shì)是“先扣名額再插訂單兩步放在一個(gè)事務(wù)里”。扣名額用的是條件 UPDATE而不是先 SELECT 再 UPDATE。app.post(/api/booking) def create_booking(): uid get_login_user_id() period_id request.json.get(period_id) date request.json.get(date) # 原子扣減只有 booked capacity 時(shí)才會(huì)更新成功 result db.execute( UPDATE period SET booked booked 1 WHERE id %s AND date %s AND booked capacity , (period_id, date) ) if result.rowcount 0: return jsonify(code409, msg該時(shí)段已被約滿請(qǐng)選擇其他時(shí)段) # 扣減成功后才允許插入預(yù)約記錄 db.execute( INSERT INTO booking (user_id, period_id, status, create_time) VALUES (%s, %s, confirmed, NOW()) , (uid, period_id) ) db.commit() return jsonify(code0, msg預(yù)約成功)這段代碼的精髓在 WHERE booked capacity。MySQL 執(zhí)行 UPDATE 時(shí)會(huì)對(duì)命中的行加行鎖兩個(gè)請(qǐng)求同時(shí)進(jìn)來(lái)只有一個(gè)能成功更新另一個(gè) rowcount 為 0直接返回約滿。這比“SELECT 出來(lái)發(fā)現(xiàn)還有名額然后 INSERT”安全得多因?yàn)?SELECT 和 INSERT 之間有一個(gè)時(shí)間窗口并發(fā)請(qǐng)求會(huì)同時(shí)通過(guò)檢查。如果你拿到的源碼是 SELECT INSERT 模式務(wù)必改成這種條件 UPDATE。事務(wù)也別忘了UPDATE 成功但 INSERT 失敗時(shí)必須回滾否則會(huì)出現(xiàn)“名額扣了但訂單沒(méi)生成”的臟數(shù)據(jù)。4.3 取消預(yù)約與后臺(tái)統(tǒng)計(jì)狀態(tài)回滾和 GROUP BY 查詢?nèi)∠A(yù)約是另一個(gè)容易出狀態(tài) bug 的地方。取消不只是把 booking 表的 status 改成 cancelled還必須把 period 表的 booked 減回去。而且不是所有訂單都能取消已經(jīng)完成的訂單就不能再釋放名額。app.post(/api/booking/cancel) def cancel_booking(): uid get_login_user_id() booking_id request.json.get(booking_id) # 只有 pending 或 confirmed 狀態(tài)可以被取消 result db.execute( UPDATE booking SET status cancelled WHERE id %s AND user_id %s AND status IN (pending, confirmed) , (booking_id, uid) ) if result.rowcount 0: return jsonify(code409, msg當(dāng)前狀態(tài)不允許取消) # 釋放對(duì)應(yīng)時(shí)段的名額 db.execute( UPDATE period p SET p.booked p.booked - 1 WHERE p.id (SELECT period_id FROM booking WHERE id %s) , (booking_id,) ) db.commit() return jsonify(code0, msg已取消)這里故意用了兩步操作來(lái)演示但兩個(gè) UPDATE 必須在同一事務(wù)里。如果把狀態(tài)更新和名額釋放拆成兩個(gè)接口一旦第二步失敗這個(gè)時(shí)段就永遠(yuǎn)少一個(gè)名額。后臺(tái)統(tǒng)計(jì)相對(duì)簡(jiǎn)單核心是 GROUP BY 查詢。比如按教練統(tǒng)計(jì)本月確認(rèn)訂單數(shù)、按日期統(tǒng)計(jì)每天預(yù)約量這些 SQL 可以直接拿去做答辯用的圖表數(shù)據(jù)源。SELECT c.name, COUNT(b.id) AS booking_count FROM booking b JOIN period p ON b.period_id p.id JOIN coach c ON p.coach_id c.id WHERE b.status confirmed AND p.date BETWEEN 2025-06-01 AND 2025-06-30 GROUP BY c.id ORDER BY booking_count DESC;5. 避坑與常見(jiàn)問(wèn)題排查畢設(shè)演示前最容易翻車的五處5.1 同一時(shí)段被約兩次并發(fā)窗口期現(xiàn)象兩個(gè)學(xué)員同時(shí)點(diǎn)“提交預(yù)約”后端兩條日志都顯示成功但時(shí)段容量只有 1。原因后端用的是先查詢剩余名額再插入訂單兩個(gè)請(qǐng)求在查詢時(shí)都看到了剩余 1于是都通過(guò)了校驗(yàn)。解決改成條件 UPDATE讓數(shù)據(jù)庫(kù)自己判斷 booked capacity同時(shí)給 booking 表加唯一索引兜底。血淚經(jīng)驗(yàn)是不要相信前端按鈕的 disabled 狀態(tài)用戶換一臺(tái)手機(jī)同時(shí)操作時(shí)前端限制完全失效。5.2 手機(jī)號(hào)解密失敗session_key 失效的三種典型現(xiàn)象真機(jī)上點(diǎn)“手機(jī)號(hào)快捷登錄”后端報(bào)解密失敗或 code 無(wú)效。第一種原因是開(kāi)發(fā)者工具里返回的模擬數(shù)據(jù)后端拿去換手機(jī)號(hào)當(dāng)然失敗第二種原因是前端先調(diào)了 wx.login拿到 code 后沒(méi)有立刻發(fā)給后端而是隔了一段時(shí)間再發(fā)code 過(guò)期第三種原因是同一份 code 被重復(fù)提交微信側(cè)規(guī)定二次換手機(jī)號(hào)的 code 直接失效。解決真機(jī)調(diào)試為主code 拿到后立刻請(qǐng)求后端后端把換到的 session_key 緩存下來(lái)但不要把 code 存數(shù)據(jù)庫(kù)重復(fù)使用。很多人卡在這一步幾天其實(shí)只是工具和真機(jī)的差異。5.3 取消預(yù)約后名額沒(méi)釋放狀態(tài)機(jī)缺了邊界判斷現(xiàn)象學(xué)員取消預(yù)約成功但其他學(xué)員看這個(gè)時(shí)段仍然是“已約滿”。原因取消接口只改了 booking 的狀態(tài)沒(méi)有把 period 表的 booked 減一或者減名額時(shí)沒(méi)判斷原訂單是否已確認(rèn)把已完成訂單的名額也減掉了。解決取消動(dòng)作放在一個(gè)事務(wù)里先更新 booking 狀態(tài)再聯(lián)動(dòng)更新 period 名額。判斷“可取消”的條件要寫(xiě)清楚只允許 pending 和 confirmed 兩種狀態(tài)取消completed 狀態(tài)直接拒絕。5.4 自定義導(dǎo)航欄頂部高度膠囊位置適配現(xiàn)象在 iPhone 上頁(yè)面正常在安卓機(jī)上頂部的標(biāo)題和返回按鈕被狀態(tài)欄擋住或者右側(cè)按鈕和微信膠囊重疊。原因頁(yè)面用了自定義導(dǎo)航欄但導(dǎo)航欄高度寫(xiě)死成了某個(gè)固定值。解決用微信官方 API 動(dòng)態(tài)計(jì)算。const menuRect wx.getMenuButtonBoundingClientRect(); const systemInfo wx.getWindowInfo(); const navBarHeight (menuRect.top - systemInfo.statusBarHeight) * 2 menuRect.height; const statusBarHeight systemInfo.statusBarHeight;menuRect 拿的是微信膠囊按鈕的位置信息statusBarHeight 是狀態(tài)欄高度兩者組合出來(lái)的 navBarHeight 才是自定義導(dǎo)航欄應(yīng)該占的高度。這套公式在主流機(jī)型上都適用比寫(xiě)死 44 或者 64 靠譜得多。頁(yè)面樣式里再用 CSS 變量把這兩個(gè)值傳進(jìn)去讓導(dǎo)航欄內(nèi)部元素動(dòng)態(tài)定位。5.5 圖庫(kù)撐爆 2MB 包體base64 圖害人現(xiàn)象開(kāi)發(fā)者工具編譯時(shí)提示主包體積超過(guò) 2MB上傳不了更沒(méi)法生成體驗(yàn)版二維碼。原因源碼圖片資源直接以 base64 字符串塞在 JS 或 WXML 里一張圖動(dòng)輒幾百 KB兩三張就把包體撐爆。解決圖片打包到本地時(shí)用壓縮工具壓一遍能走 CDN 的走 CDN不能走 CDN 的放進(jìn)分包tabBar 圖標(biāo)控制在 40KB 以內(nèi)頁(yè)面插圖控制在 100KB 以內(nèi)。還有一個(gè)容易被忽略的坑開(kāi)發(fā)者在工具里開(kāi)啟“不校驗(yàn)合法域名”能正常請(qǐng)求本地接口但真機(jī)預(yù)覽時(shí)必須把后端域名配到小程序后臺(tái)的 request 合法域名里否則所有請(qǐng)求直接失敗這個(gè)問(wèn)題和包體大小一樣都是演示當(dāng)天才發(fā)現(xiàn)就來(lái)不及改的。6. 從“能演示”到“敢答辯”一小時(shí)驗(yàn)證核心預(yù)約鏈路拿到源碼后別急著改界面先花一個(gè)小時(shí)驗(yàn)證預(yù)約主鏈路是否真的可靠。第一步準(zhǔn)備兩個(gè)微信號(hào)一個(gè)當(dāng)學(xué)員一個(gè)當(dāng)管理員或教練真機(jī)預(yù)覽跑一遍“登錄 → 選時(shí)段 → 提交預(yù)約 → 取消預(yù)約 → 后臺(tái)確認(rèn)”的完整流程把每一步的截圖和接口返回時(shí)間記錄下來(lái)這些內(nèi)容可以直接進(jìn)論文的測(cè)試章節(jié)。第二步驗(yàn)證并發(fā)場(chǎng)景。沒(méi)有兩臺(tái)手機(jī)就用命令行模擬后端地址換成你自己的接口循環(huán)發(fā)起預(yù)約請(qǐng)求觀察返回值。for i in $(seq 1 20); do curl -s -X POST http://localhost:5000/api/booking \ -H Authorization: Bearer test-token-$i \ -H Content-Type: application/json \ -d {period_id: 1, date: 2025-06-10} done如果后端正確實(shí)現(xiàn)了條件 UPDATE這 20 個(gè)請(qǐng)求里應(yīng)該只有 1 個(gè)返回成功其余返回約滿提示。如果返回了多個(gè)成功說(shuō)明核心邏輯有問(wèn)題必須在演示前修掉。第三步在微信開(kāi)發(fā)者工具里打開(kāi)性能面板跑一遍頁(yè)面加載和預(yù)約請(qǐng)求看耗時(shí)和報(bào)錯(cuò)。很多人答辯被問(wèn)倒不是功能不對(duì)而是說(shuō)不出“你的系統(tǒng)能扛住幾個(gè)人同時(shí)約”這一小時(shí)的并發(fā)測(cè)試就是你的底氣。我當(dāng)年第一次跑這種腳本時(shí)后端直接返回了十幾條成功記錄現(xiàn)場(chǎng)改代碼改到冒汗。所以現(xiàn)在拿到任何一套駕校預(yù)約管理系統(tǒng)源碼我第一件事永遠(yuǎn)是測(cè)并發(fā)而不是改 UI。代碼可以丑數(shù)據(jù)庫(kù)可以亂預(yù)約鏈路的原子性不能垮。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取