:并發(fā)控庫與核銷閉環(huán)實戰(zhàn))
簡介面向微信小程序開發(fā)學習者與Java全棧初學者這是一份駕校預約管理系統(tǒng)的完整源碼包。項目基于微信開發(fā)者工具后端采用SSM框架與MySQL數(shù)據(jù)庫B/S架構分為學員、教練、管理員三類角色包含首頁、個人中心、學員管理、教練管理、駕校信息管理、駕校車輛管理、教練預約管理、考試信息管理、預約考試管理、系統(tǒng)管理等模塊游客無需登錄也可瀏覽公開信息。壓縮包共1209個文件以png、js、vue、java、json等類型為主涵蓋前端圖標、小程序頁面、Vue后臺組件、Java接口與項目配置總大小12.28MB。目前已有463人下載學習。配套說明文檔與一鍵安裝、運行、構建腳本便于快速啟動項目資源目錄劃分清晰適合課程設計、畢業(yè)設計以及想理解小程序預約類系統(tǒng)完整實現(xiàn)流程的讀者參考。1. 駕校預約管理系統(tǒng)不是“做幾個頁面”是讓教練、時段、庫存和核銷轉(zhuǎn)成一條閉環(huán)駕校預約管理系統(tǒng)聽起來像是“選個教練、挑個時間、存一條記錄”但真拿去給駕校用你會發(fā)現(xiàn)最核心的需求是學員能自助約課、改期、取消教練能看到自己一天什么時候被約了前臺要能核銷、統(tǒng)計、處理爽約。這個標題里的小程序項目就是把這一整套流程壓縮到微信里學員不裝 App、教練不拿紙質(zhì)表打開小程序就能操作。對剛接觸小程序開發(fā)的人來說它是一個比“電商練手”更貼近真實業(yè)務的樣本對正在接駕校、培訓機構私活的人來說它直接對得上驗收清單。真正讓這類項目翻車的往往不是頁面難看而是“同一個時段被兩個人同時約上”這種并發(fā)問題以及預約狀態(tài)一亂、核銷對不上賬的連鎖反應。下面按我自己的落地習慣把這套系統(tǒng)的選型、表結(jié)構、前端實現(xiàn)和排錯順序拆開講。2. 技術棧選型原生微信小程序還是 uni-app駕校預約為什么不能一上來就寫頁面2.1 原生微信小程序與 uni-app 的真實差異拿到“微信小程序項目之駕校預約管理系統(tǒng)”這種源碼包第一件事不是打開開發(fā)工具而是確定這套前端是用原生微信小程序?qū)懙倪€是 uni-app 轉(zhuǎn)出來的。兩者在工程結(jié)構上差別很大原生工程里你會看到pages、app.js、app.json直接導入微信開發(fā)者工具就能跑uni-app 工程則是src/pages加uni_modules需要先npm install、再用 HBuilderX 或 CLI 打包成微信小程序產(chǎn)物目錄才是開發(fā)者工具能認的。從落地角度說駕校預約這種業(yè)務基本只發(fā)生在微信生態(tài)里學員掃駕校的碼就進小程序不存在“同一個人還要在支付寶里再約一次”的強需求。所以我一般建議除非你一早確定要同時發(fā)抖音、支付寶、微信三端否則不要為了“多端”硬上 uni-app。uni-app 確實能把一套 Vue 語法編譯到多端但代價是每次改完都要走一次“打包”流程遇到自定義組件、分包加載、原生插件時調(diào)試鏈路比原生長一截。很多新手在 uniapp 微信小程序打包時踩到source size 2612kb exceed max limit 2mb就是因為沒有意識到小程序主包有 2MB 硬限制把圖片、SDK、公共庫全塞進了一個包里。反過來說如果這個駕校預約項目是課程設計要求里面有“管理后臺 小程序 數(shù)據(jù)庫設計文檔”那 uni-app 也不是不能用因為后臺界面可以復用同一套 Vue 代碼做成 H5省得再寫一套。但你要接受一個事實出問題的人多數(shù)不在業(yè)務邏輯而在“打包出的包太大”或“原生組件不兼容”。所以我的選擇順序是單純做微信端、工期緊、想少踩坑用原生要交作業(yè)展示多端能力、后臺也要復用才考慮 uni-app并且從第一天就做分包。2.2 后端選型PHP / Java / Node 在預約場景里的取舍前端只是入口預約管理系統(tǒng)真正的體力活在接口層。常見的源碼包里后端出現(xiàn)頻率最高的是 PHP 和 Java Spring Boot也有 Node 寫的。駕駛培訓行業(yè)的小型駕校通常已經(jīng)是 PHP 或 Java 的存量系統(tǒng)小程序只是新加一個 C 端入口這時后端語言沒得選要順著老系統(tǒng)來。如果是從零開始我的建議是按你的維護能力選而不是按“哪個熱門”選。PHP 的優(yōu)勢是部署門檻低隨便一臺虛擬主機就能跑很多源碼包的說明文檔也是以 PHP MySQL 為主線適合課程設計或者小駕校自用。Java Spring Boot 適合要接大量并發(fā)、要寫復雜報表、要把預約和財務對賬打通的情況但環(huán)境要求高Maven 依賴一堆新手第一次跑起來容易耗一小時在配置上。Node 適合本來就會 JavaScript 的人寫接口快但線上部署和進程守護要自己多留心。需要強調(diào)的是預約系統(tǒng)對后端的要求不只是“能存數(shù)據(jù)”而是“并發(fā)下不超賣”這一點跟語言無關跟事務和鎖的寫法有關。所以我會先把后端能力當成“黑匣子”測試用并發(fā)請求打它而不是看它用了什么框架。2.3 拿到“源碼說明”包后先做三件事看文檔、倒數(shù)據(jù)庫、對接口約定標題里帶著“(源碼說明)”說明包里應該有配套文檔。我拿到這類項目包的習慣是先翻說明再看數(shù)據(jù)庫腳本最后才導入代碼。很多人的誤區(qū)是先把小程序跑起來結(jié)果白屏了才想起去看接口地址浪費半小時。說明文檔里通常會寫清楚三件事數(shù)據(jù)庫初始化腳本在哪、后臺賬號密碼是什么、小程序需要改哪些配置項。這些信息不全時我會先按目錄猜常見結(jié)構是server放后端、sql放建表語句、docs放設計文檔。數(shù)據(jù)庫腳本導入后我會重點核對幾張表用戶表、教練表、課時表、預約表。駕校預約的核心資源不是學員而是“教練 車 時段”的組合。如果表里沒有記錄“每個時段最多可預約人數(shù)”的字段那這個源碼包的預約邏輯大概率是閹割版——先查再插入并發(fā)一高就會重復預約。這一步確認清楚后面所有頁面開發(fā)才會踏實。小程序端要改的無非是app.js里的request域名、登錄密鑰以及每個接口的url前綴。記住源碼包能不能落地不在代碼多高級而在接口約定和數(shù)據(jù)字典能不能對上。3. 數(shù)據(jù)模型與預約狀態(tài)機先把“庫存”算清楚再寫接口3.1 駕校預約管理系統(tǒng)的表結(jié)構設計預約系統(tǒng)最怕“看起來像論壇約個課就是插一條記錄”。如果約課只是往appointment表里 insert那同一時間教練就會被兩個人同時約走因為誰都沒有檢查“這個時段還剩幾個名額”。所以第一步是設計出能反映資源庫存的表結(jié)構。駕校的核心資源是教練和教練車一個教練一天可以分多個時段每個時段理論上只能服務固定數(shù)量學員通常是一對一。我常用的建表語句如下CREATE TABLE coach ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1可用 0停用 ); CREATE TABLE course_schedule ( id INT PRIMARY KEY AUTO_INCREMENT, coach_id INT NOT NULL, course_date DATE NOT NULL, slot_id INT NOT NULL COMMENT 時段編號 1上午 2下午 3晚上, capacity INT DEFAULT 1 COMMENT 該時段可約人數(shù), booked INT DEFAULT 0 COMMENT 已約人數(shù), version INT DEFAULT 0 COMMENT 樂觀鎖版本號, UNIQUE KEY uk_date_slot (coach_id, course_date, slot_id) ); CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, schedule_id INT NOT NULL, status TINYINT DEFAULT 1 COMMENT 1已預約 2已取消 3已完成 4爽約, cancel_reason VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_schedule (student_id, schedule_id) );這套結(jié)構強調(diào)三件事。第一course_schedule是庫存表capacity和booked分別代表總量和已約量每次預約都要保證booked capacity。第二教練、日期、時段三個字段組成唯一索引防止同一批數(shù)據(jù)里出現(xiàn)重復排課。第三appointment表里不直接存教練和時段而是存schedule_id這樣改排課表的時候預約記錄不用跟著動。version字段先留著后面做樂觀鎖要用。實際項目里我還會加一個cancel_log表記錄取消操作因為駕校經(jīng)常需要知道“誰頻繁爽約、哪個時段的取消率最高”。這不算過度設計是運營必須的數(shù)據(jù)。你要是嫌表多起碼也要把appointment里的狀態(tài)字段留出來否則后面做核銷的時候會發(fā)現(xiàn)根本沒有狀態(tài)可流轉(zhuǎn)。3.2 防止重復預約的原子扣減與狀態(tài)流轉(zhuǎn)預約接口寫得好不好直接在并發(fā)下見分曉。最常見的錯誤寫法是先SELECT booked FROM course_schedule WHERE id ?判斷小于 capacity再INSERT INTO appointment最后UPDATE course_schedule SET booked booked 1。這一步看代碼沒毛病但兩個學員同時請求時兩個請求都讀到booked0然后都插入預約記錄最后都執(zhí)行加一結(jié)果這里約滿了兩個人實際上超賣了。正確做法是把“檢查庫存 扣減”放在一個原子操作里用數(shù)據(jù)庫的行鎖保證同一時刻只有一個請求能成功扣減UPDATE course_schedule SET booked booked 1 WHERE id ? AND booked capacity;這條 UPDATE 影響行數(shù)為 1說明扣減成功才允許繼續(xù)插入預約記錄影響行數(shù)為 0說明時段已滿直接返回“該時段已約滿”??弁陰齑嬖俨迦腩A約并且這兩個動作要在同一個事務里任何一步失敗都要回滾讓booked恢復原樣。后端代碼里大概是這個形態(tài)// 以 Node 為例使用 mysql2 驅(qū)動 const conn await pool.getConnection(); try { await conn.beginTransaction(); const [result] await conn.execute( UPDATE course_schedule SET booked booked 1 WHERE id ? AND booked capacity, [scheduleId] ); // affectedRows 1 才代表搶到了名額 if (result.affectedRows 0) { await conn.rollback(); return { code: 1, message: 該時段已約滿請換一個時段 }; } await conn.execute( INSERT INTO appointment (student_id, schedule_id, status) VALUES (?, ?, 1), [studentId, scheduleId] ); await conn.commit(); return { code: 0, message: 預約成功 }; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); }參數(shù)說明affectedRows是 MySQL 執(zhí)行 UPDATE 后返回的受影響行數(shù)它能告訴你這個 UPDATE 是不是真正改到了數(shù)據(jù)。這里的scheduleId是預約接口傳入的排課記錄 IDstudentId是登錄后服務端解析出來的用戶 ID不能拿前端傳的 userId 直接用不然學員可以把別人的 ID 塞進來替別人約課。事務的beginTransaction和commit保證“扣庫存”和“建預約”要么同時成功要么同時失敗。3.3 取消預約與時段過期庫存回滾和違約標記怎么處理有預約就必然有取消取消邏輯看起來只是把appointment的狀態(tài)改成“已取消”但別忘了把庫存還回去。這一步同樣要放在事務里先改預約狀態(tài)再booked booked - 1。順序別反不然出現(xiàn)“狀態(tài)已取消但庫存沒恢復”的半截數(shù)據(jù)后面統(tǒng)計約滿率全錯。駕校場景比較特殊的是“爽約”的處理。學員約了早上八點的課教練把車和人都空出來了學員沒來這個時段已經(jīng)不可能再補給別人所以不能簡單地取消并回滾庫存而是要標記為“爽約”系統(tǒng)可以記一次違約累計多次限制預約。實現(xiàn)上我一般會根據(jù)當前時間判斷距離預約時段開始還有足夠時間比如 4 小時允許正常取消并回滾庫存如果已進入倒計時或者已經(jīng)過了開始時間就只能由管理員手動取消學員端只能看到“已過期”狀態(tài)。這種規(guī)則不寫死在 SQL 里而是由后端在取消接口里判斷前端只負責引導用戶確認。狀態(tài)流轉(zhuǎn)的完整鏈條是已預約 → 已取消 / 已完成 / 爽約。核銷動作只允許把已預約變成已完成取消動作只允許把已預約變成已取消。如果表里出現(xiàn)“已取消”直接跳到“已完成”的記錄基本可以判定接口邏輯里少了狀態(tài)校驗。調(diào)試的時候可以在后端加一個狀態(tài)機的校驗函數(shù)每次更新狀態(tài)時檢查“當前狀態(tài)是否允許跳到目標狀態(tài)”不滿足就報錯。這一步能擋住大多數(shù)臟數(shù)據(jù)。4. 小程序端從登錄到核銷手寫核心頁面與請求邏輯4.1 微信小程序登錄獲取手機號兩種方式怎么選駕校預約一定要拿到學員手機號因為預約成功要發(fā)通知、教練要聯(lián)系學員確認上車點。微信小程序登錄獲取手機號有兩種常見方式選錯會直接影響審核和上線。第一種是傳統(tǒng)方式用wx.login拿到臨時code發(fā)給后端后端調(diào)用微信接口換openid然后讓學員在小程序里手填手機號后端發(fā)驗證碼。這種方式實現(xiàn)簡單、不用企業(yè)認證也能測通但用戶體驗多了一步并且驗證碼短信有成本。第二種是使用微信提供的手機號快速驗證組件學員點一下授權按鈕微信直接把手機號傳給后端體驗最好但要求小程序必須完成認證且該能力對個人主體小程序不開放。很多個人開發(fā)者在這里會被卡住審核時發(fā)現(xiàn)沒有權限只能退回手填手機號。我這里建議如果是公司主體的駕校項目優(yōu)先用手機號快速驗證組件如果是個人項目或畢設就老老實實走“登錄 手填手機號 短信驗證”。核心代碼大致是// 前端手機號快速驗證組件 button open-typegetPhoneNumber bindgetphonenumberonGetPhone 獲取手機號 /button// 前端拿到加密數(shù)據(jù)后交給后端解析 Page({ async onGetPhone(e) { if (e.detail.errMsg ! getPhoneNumber:ok) { wx.showToast({ title: 需要授權手機號, icon: none }); return; } const res await wx.cloud.callFunction({ name: decodePhone, data: { code: e.detail.code } }); // res.result.phone 就是手機號 } });參數(shù)說明e.detail.code是微信返回的動態(tài)令牌只能在后端配合 session_key 換取手機號不能在前端直接讀出手機明文。如果是普通 HTTP 后端前端只能把code原樣 POST 給服務端服務端再按文檔調(diào)接口。這里最容易踩的坑是把openid和手機號混為一談OpenID 只能標識“這是哪個微信用戶”不能當手機號用。4.2 選教練、選時段、提交預約的核心代碼預約頁面的交互主線是選日期 → 選教練 → 選時段 → 提交。提交前要把日期、教練、時段拼成可校驗的三個參數(shù)傳給后端后端再拿著教練 ID 和日期去查排課表。前端代碼不復雜但“重復提交”這個坑必須防用戶手快點兩次按鈕就可能發(fā)出兩個相同請求即使后端有庫存扣減兜底也會產(chǎn)生兩條預約記錄。所以提交按鈕要在請求發(fā)出后立即禁用成功或失敗后再恢復。!-- wxml 片段 -- view classslot-list view wx:for{{slots}} wx:keyslotId classslot-item {{item.disabled ? disabled : }} >Page({ data: { date: , coachId: , slotId: , submitting: false }, onSelectSlot(e) { this.setData({ slotId: e.currentTarget.dataset.id }); }, async onSubmit() { const { date, coachId, slotId, submitting } this.data; if (submitting) return; // 重復提交攔截 if (!date || !coachId || !slotId) { wx.showToast({ title: 請選擇完整, icon: none }); return; } this.setData({ submitting: true }); try { const res await wx.request({ url: https://api.example.com/appointment/create, method: POST, data: { date, coachId, slotId } }); if (res.data.code 0) { wx.showToast({ title: 預約成功, icon: success }); // 跳轉(zhuǎn)到預約記錄頁 } else { wx.showToast({ title: res.data.message, icon: none }); } } finally { this.setData({ submitting: false }); } } });這里的date是形如2025-06-15的日期字符串slotId是排課表里具體的時段 ID而不是“上午”“下午”這種前端自己翻譯出來的值。后端必須用slotId coachId作為唯一鍵去查course_schedule不要用日期加中文時段名去拼否則數(shù)據(jù)庫里存什么你會徹底失控。wx.request的url必須是https且在微信公眾平臺配過合法域名開發(fā)時可以在開發(fā)者工具里關掉域名校驗但真機預覽就一定要配好。4.3 自定義導航欄與頁面?zhèn)鲄㈨敳繉Ш綑诟叨仍趺催m配預約系統(tǒng)里“本周日期橫滑選擇器”很常見這種頁面通常想用自定義導航欄才能把標題欄和選擇器做成一體。自定義導航欄不是把navigationStyle改成custom就完事真正麻煩的是狀態(tài)欄高度和膠囊按鈕位置在不同的手機上都不同適配不好就會出現(xiàn)標題頂?shù)綌z像頭、按鈕錯位的情況。微信沒有直接暴露狀態(tài)欄高度一般用wx.getMenuButtonBoundingClientRect()拿膠囊按鈕的位置再結(jié)合wx.getSystemInfoSync()里的statusBarHeight算導航欄總高度。常見做法是const rect wx.getMenuButtonBoundingClientRect(); const system wx.getSystemInfoSync(); const navHeight rect.bottom (rect.top - system.statusBarHeight) * 2;這段代碼里navHeight是自定義導航欄的推薦高度單位是 px。rect.top是膠囊到屏幕頂部的距離system.statusBarHeight是系統(tǒng)狀態(tài)欄高度兩者的差值就是膠囊上方的留白乘以 2 再加膠囊底部的留白基本能得到一個視覺協(xié)調(diào)的導航欄高度。拿到這個值后設置成外層容器的padding-top整個頁面內(nèi)容就不會被劉海屏吞掉。很多參考項目里寫死44px在全面屏上就會顯得局促這是微信小程序頂部導航欄高度這個熱搜詞背后最常被問的問題。字段傳參方面從列表頁跳到詳情頁參數(shù)復雜時要用encodeURIComponent包一層防止特殊字符截斷。5. 預約系統(tǒng)的常見坑與排錯從白屏到重復預約5.1 現(xiàn)象真機預覽白屏模擬器正常模擬器里能正常打開真機預覽卻是白屏第一反應不應該是查代碼而是查“請求域名”。微信開發(fā)者工具默認勾選了“不校驗合法域名”所以工具里請求任何 HTTP 接口都能通甚至localhost也行。真機上這個校驗是強制生效的未配置的域名直接請求失敗頁面拿不到數(shù)據(jù)就白在那里。解決方法是登錄微信公眾平臺在“開發(fā)管理 → 服務器域名”里把后端接口的 HTTPS 域名加到request合法域名列表而且域名必須是備案過的不能是 IP。如果你只是臨時演示也可以打開開發(fā)者工具的真機調(diào)試并在“項目設置”里勾選“不校驗合法域名”但這樣只能給你自己看發(fā)給別人一樣白屏。這個坑在校驗域名前后最容易讓人誤判成“代碼寫錯了”其實后端日志里全是 CORS 或證書報錯。5.2 現(xiàn)象兩個學員“同時”約到了同一個時段約同一個時段兩個人都收到“預約成功”這是并發(fā)問題最典型的形態(tài)。原因基本可以鎖定為后端先 SELECT 再 INSERT沒有把“檢查名額”和“扣減名額”做成原子操作。兩條請求同時讀到booked0都認為自己還有名額于是都插入成功。解決辦法就是把 3.2 里的原子 UPDATE 用起來配合事務。如果業(yè)務太復雜一條 SQL 解決不了也可以給course_schedule表加樂觀鎖UPDATE 時帶version當前版本更新成功后version1失敗就重試。但原子 UPDATE 已經(jīng)足夠覆蓋駕校預約這種量級。修復后一定要用并發(fā)工具跑一次復測見 6.1。5.3 現(xiàn)象日期選完列表不刷新約到的還是前一天時段列表沒跟著日期變多半是頁面把第一次加載的數(shù)據(jù)緩存住了或者change事件里用的日期格式不對。常見原因有兩個一是日期組件返回的是new Date()對象直接setData后變成了字符串接口收到后解析出“昨天”二是小程序頁面棧的上一頁沒有在onShow里重新拉取數(shù)據(jù)導致返回時列表還是舊的。解決方法是前端先把日期格式化成YYYY-MM-DD再傳給后端預約記錄頁在onShow里重新調(diào)用列表接口而不是依賴onLoad只加載一次。如果后端返回的日期比前端少了 8 小時那就是時區(qū)問題統(tǒng)一在接口層約定按YYYY-MM-DD的字符串傳參不要傳“相對當前時間”的時間戳。5.4 現(xiàn)象體驗版登錄不上后端一直報 code 無效wx.login返回的code是有時效且只能用一次的。如果你后端的登錄解密邏輯把同一個code連續(xù)拿去換openid第二次就會報“code 無效”。還有一個很隱蔽的問題開發(fā)者工具里的appid和體驗版所屬的appid不一致前端拿到的是 A 項目的 code后端用了 B 項目的密鑰去解必然失敗。解決方法是先核對小程序后臺的 appid 和后端配置的 appid 是否一致再檢查登錄接口是否對同一個 code 做了緩存。最容易忽略的是一鍵登錄按鈕被用戶點了兩遍前端每點一次就調(diào)一次wx.login但用戶心態(tài)是“手機號沒出來就再點一下”導致舊 code 被重復使用。前端要做防重復點擊后端要做 code 消費失敗時的友好報錯。5.5 現(xiàn)象uniapp 打包后主包超過 2MB 上限如果你用的是 uni-app編譯成微信小程序時經(jīng)常看到source size 2612kb exceed max limit 2mb。這不代表你的業(yè)務有多龐大而是很多依賴被打進了主包。最常見的是把完整的uview、colorui這類 UI 庫全量引入或者把后端 SDK、加密庫都塞進main.js再或者圖片直接 base64 進代碼里。解決思路是分包、抽組件、圖片走 CDN。微信小程序主包只放頁面骨架和公共依賴教練詳情、預約記錄、用戶中心這些低頻頁面全部放到subpackages里從頁面名到資源路徑都要對應調(diào)整。UI 庫改成按需引入如果只是用幾個組件復制對應文件過來反而比全量引入更小。這里的取舍是主包大小決定首屏加載速度分包越干凈真機體驗越順調(diào)試時也不至于為 2MB 邊界反復重構。6. 讓預約系統(tǒng)經(jīng)得起真車流量壓測、訂閱消息與核銷閉環(huán)6.1 用最簡單的方式給預約接口做并發(fā)壓測??荚囶A約、寒暑假約車高峰期流量會突然漲起來。不要等到上線當天才發(fā)現(xiàn)接口扛不住用wrk或者一段帶Promise.all的 Node 腳本就能模擬并發(fā)。重點壓兩個接口創(chuàng)建預約和取消預約。壓測時關注三件事有沒有超賣、響應時間是否穩(wěn)定、數(shù)據(jù)庫連接池有沒有爆。命令行跑法如下wrk -t4 -c50 -d10s -s create-appointment.lua http://api.example.com/appointment/create壓測腳本會帶不同studentId并發(fā)發(fā)起預約。跑完不要只盯著 QPS 數(shù)字而是對數(shù)據(jù)庫查一遍course_schedule的booked是否等于appointment表里狀態(tài)為“已預約”的記錄數(shù)。對不上就是邏輯有洞對得上并且響應時間控制在幾百毫秒內(nèi)才算過基本門檻。這個測試要留著每次都跑比改完代碼拍胸脯有用得多。6.2 訂閱消息預約成功提醒的正確觸發(fā)方式駕校預約成功后學員需要收到“預約成功”“明日練車提醒”之類的通知。微信官方現(xiàn)在把模板消息換成了訂閱消息關鍵限制是小程序必須在用戶點擊按鈕時調(diào)wx.requestSubscribeMessage不能在接口返回成功后再偷偷彈窗。也就是說提交預約的按鈕事件里應該先讓用戶確認訂閱授權再發(fā)預約請求。async onSubmit() { await wx.requestSubscribeMessage({ tmplIds: [預約成功通知模板ID] }); // 用戶選“總是保持以上選擇”或點允許后再走預約接口 this.doCreateAppointment(); }注意這個授權結(jié)果只代表“下一次消息可以發(fā)”不能存下來長期發(fā)。如果用戶拒絕預約流程也要繼續(xù)只是不發(fā)通知。所以這里不要因為用戶沒點允許就中斷預約而是把它做成一個增強體驗。真正的重要信息比如“教練臨時請假”還需要在后端做二次確認機制不能只依賴訂閱消息。6.3 二維碼核銷與取消預約的回滾順序教練端“掃一掃核銷”是這個系統(tǒng)的最后一環(huán)。學員預約成功后后端生成一個帶預約號或簽名字符串的二維碼教練用小程序的掃一掃功能讀取再調(diào)核銷接口。核銷接口必須冪等同一個二維碼掃第二次后端要返回“該預約已完成”而不是報錯或再次核銷成功。實現(xiàn)上預約號用隨機串生成數(shù)據(jù)庫里加唯一索引核銷時先查狀態(tài)再原子更新UPDATE appointment SET status 3 WHERE id ? AND status 1;這條 SQL 只有狀態(tài)為“已預約”的記錄會被更新成“已完成”影響行數(shù)為 0 就說明狀態(tài)已變化直接給前端返回“不可重復核銷”。至于取消預約的回滾順序一定是先改預約狀態(tài)為“已取消”再讓course_schedule的booked減一。如果先減庫存再改狀態(tài)中間出現(xiàn)異常就會出現(xiàn)“約了課的人沒記錄、名額卻空出來”的永久性臟數(shù)據(jù)。把這兩個操作放進同一個事務讓 MySQL 替你保證要么都成功、要么都失敗。我做第一個預約項目時就吃過這個虧取消了三條記錄后庫存全部對不上只能手動寫腳本清洗那感覺比改代碼痛苦得多。這些邊界問題都處理完這套駕校預約管理系統(tǒng)才算真正能交付而不是只能在模擬器里點兩下。希望幫到你。本文還有配套的精品資源點擊獲取