約系統(tǒng)實(shí)戰(zhàn):排期建模與并發(fā)搶場方案)
去年幫一位做球館運(yùn)營的朋友落地了一套預(yù)約管理系統(tǒng)前端是微信小程序后端是Java整體跑通之后我才敢說這類看似很簡單的預(yù)約系統(tǒng)真正做起來全是細(xì)節(jié)。用戶選場地、選時(shí)間、下單支付、管理員核銷聽起來就是四個(gè)步驟但背后涉及場地與時(shí)段的多維度建模、并發(fā)訂場下的數(shù)據(jù)一致性、微信登錄與支付的接入、小程序端各種設(shè)備適配任何一個(gè)環(huán)節(jié)沒想清楚后面返工的成本都極高。這篇文章不是課程式教學(xué)而是把我從零搭建這套Java基于微信小程序的球館預(yù)約系統(tǒng)的完整思路、關(guān)鍵設(shè)計(jì)、踩坑記錄都寫出來配上源碼和文檔說明希望能給正在做同類項(xiàng)目的朋友省點(diǎn)時(shí)間。1. 球館預(yù)約系統(tǒng)的業(yè)務(wù)本質(zhì)與整體架構(gòu)1.1 先把場地時(shí)段的二維模型想清楚預(yù)約類系統(tǒng)的核心從來不是下單這個(gè)動(dòng)作而是排期數(shù)據(jù)怎么組織。球館預(yù)約和普通電商下單最大的區(qū)別在于商品是某個(gè)場地在某個(gè)時(shí)間段的使用權(quán)這是一個(gè)典型的二維組合模型。舉個(gè)例子一個(gè)球館有羽毛球場地A、B、C三片每天營業(yè)時(shí)間是9點(diǎn)到22點(diǎn)以1小時(shí)為最小預(yù)約單位。那么一天可售的商品就有 3片場地 × 13個(gè)時(shí)段 39個(gè)。一周就是273個(gè)。如果最小預(yù)約單位改成半小時(shí)這個(gè)數(shù)字直接翻倍。我見過很多初版設(shè)計(jì)把時(shí)段寫死成早中晚三個(gè)字段結(jié)果用戶要定晚上8點(diǎn)半到9點(diǎn)半系統(tǒng)直接改不了——這就是模型設(shè)計(jì)不到位。正確的做法是拆兩張核心表一張是場地表記錄場地基礎(chǔ)信息一張是排期表記錄某場地某天的某時(shí)段是否可約??捎脮r(shí)段在后臺提前生成用戶端只負(fù)責(zé)查和選。這樣的模型好處很多可以單獨(dú)對某個(gè)場地的某個(gè)時(shí)段做停售比如場地A在周三下午做維護(hù)、可以動(dòng)態(tài)調(diào)整價(jià)格黃金時(shí)段加價(jià)、可以輕松支持不同的預(yù)約粒度。我在文檔說明里畫的ER圖也是按這個(gè)思路來的建議所有做預(yù)約類系統(tǒng)的朋友先別急著寫代碼把這張二維組合模型想明白后面所有功能都是在這個(gè)骨架上長出來的。1.2 技術(shù)選型思路為什么是Spring Boot 原生微信小程序后端選型我用了Spring Boot MyBatis-Plus Redis MySQL前端用微信小程序原生語法。這套組合不算新潮但非常適合中小型球館預(yù)約項(xiàng)目。Spring Boot生態(tài)成熟招人容易社區(qū)資料多。MyBatis-Plus做單表CRUD效率極高尤其是分頁查詢、條件構(gòu)造器寫起來比手寫XML SQL痛快太多。Redis在這里有兩個(gè)職責(zé)一是緩存熱點(diǎn)數(shù)據(jù)比如未來兩天的場地余量二是做分布式鎖防止并發(fā)搶場。MySQL存核心業(yè)務(wù)數(shù)據(jù)排期表日增量不大壓力完全在可控范圍。小程序端我沒有選uni-app或者Taro直接用原生。原因是這個(gè)項(xiàng)目頁面量不大原生語法在微信開發(fā)者工具里的調(diào)試體驗(yàn)最穩(wěn)定而且很多隱藏坑比如導(dǎo)航欄高度適配、分享參數(shù)傳遞原生處理起來最直接。跨端框架適合大型多端項(xiàng)目但對球館預(yù)約這種只服務(wù)微信用戶的場景原生是性價(jià)比最高的選擇。1.3 目錄結(jié)構(gòu)與數(shù)據(jù)表設(shè)計(jì)項(xiàng)目采用經(jīng)典的分層結(jié)構(gòu)controller、service、mapper、entity、config、common統(tǒng)一返回體和異常處理。我習(xí)慣在common里放一個(gè)Result類所有接口統(tǒng)一返回 code message data小程序端做統(tǒng)一攔截。這樣可以避免每個(gè)接口返回結(jié)構(gòu)不一致前端解析起來像拆盲盒。數(shù)據(jù)表方面核心是下面這幾張表名用途關(guān)鍵字段venue場館信息area_type、opening_time、closing_time、addresscourt場地信息venue_id、name、statuscourt_schedule排期表court_id、date、start_time、end_time、price、statususer用戶表openid、nickname、phoneorder訂單表order_no、user_id、schedule_id、amount、status、pay_timerefund_record退款記錄order_no、refund_amount、reason、status排期表一定要加聯(lián)合唯一索引比如 (court_id, date, start_time)這是防止重復(fù)生成排期的底層保障。訂單表的 status 建議用 TinyInt 存數(shù)字狀態(tài)碼不要用字符串省空間也方便比較。這里多說一句order_no 生成不要用自增ID用時(shí)間戳隨機(jī)數(shù)拼一個(gè)20位以內(nèi)的業(yè)務(wù)單號微信支付回調(diào)時(shí)要用它做冪等匹配格式穩(wěn)定很重要。2. Java后端最容易翻車的兩個(gè)點(diǎn)時(shí)段建檔與并發(fā)訂場2.1 周循環(huán)排期用任務(wù)自動(dòng)生成一個(gè)周期內(nèi)的場地狀態(tài)排期數(shù)據(jù)不是人工一條條錄進(jìn)去的而是需要一個(gè)生成機(jī)制。我的做法是系統(tǒng)定義一個(gè)定時(shí)任務(wù)每天凌晨自動(dòng)生成未來一周的排期數(shù)據(jù)。比如今天是周一任務(wù)會(huì)把下周一之前的所有場地時(shí)段補(bǔ)充完整。如果某天場地需要維護(hù)后臺管理員可以單獨(dú)把那條排期標(biāo)記為不可約不需要改生成邏輯。生成邏輯的核心是嵌套循環(huán)先遍歷場地再遍歷營業(yè)時(shí)段逐條插入排期表。插入前先檢查這條排期是否已存在存在就跳過。這個(gè)先查再插的操作有并發(fā)風(fēng)險(xiǎn)多個(gè)定時(shí)任務(wù)實(shí)例同時(shí)跑所以我在排期表上建了上文提到的聯(lián)合唯一索引真出現(xiàn)重復(fù)插入會(huì)被數(shù)據(jù)庫直接擋掉比在代碼里加鎖簡單可靠得多。還有一個(gè)小細(xì)節(jié)跨天問題。球館如果營業(yè)到凌晨2點(diǎn)那么23:00-00:00這個(gè)時(shí)段到底屬于哪一天我統(tǒng)一按開始時(shí)間所在日期歸屬時(shí)段跨天就把結(jié)束時(shí)間直接寫成第二天的日期時(shí)間排期表里的date字段只表示這是哪一天開放的場次。這個(gè)規(guī)則一定要在文檔說明里寫清楚不然前端展示周五晚場的時(shí)候很容易錯(cuò)位。2.2 并發(fā)下黃金時(shí)段被搶的問題鎖不是越重越好球館預(yù)約最經(jīng)典的并發(fā)場景是晚上7點(diǎn)到9點(diǎn)的黃金時(shí)段同一片場地兩個(gè)人同時(shí)點(diǎn)擊預(yù)約。如果代碼是先查余量再扣減那在高并發(fā)下必然出現(xiàn)超賣——兩個(gè)人都查到了余量都下單成功。我當(dāng)時(shí)設(shè)計(jì)了三種可選方案實(shí)際選的是第二種第一數(shù)據(jù)庫樂觀鎖在排期表加一個(gè)version字段更新時(shí)帶上version條件。好處是無鎖開銷壞處是沖突的時(shí)候用戶直接報(bào)錯(cuò)體驗(yàn)比較生硬。第二Redis分布式鎖 事務(wù)以 schedule_id 為鎖鍵用戶請求時(shí)先搶鎖搶到后走下單事務(wù)事務(wù)提交后再釋放鎖。這樣可以保證同一片場地同一個(gè)時(shí)段同時(shí)只有一個(gè)請求在操作其他請求等待或快速失敗。我選這個(gè)方案是因?yàn)樗缺WC了數(shù)據(jù)一致性又不會(huì)像樂觀鎖那樣讓大量請求直接失敗。第三數(shù)據(jù)庫悲觀鎖SELECT ... FOR UPDATE實(shí)現(xiàn)最簡單但對數(shù)據(jù)庫連接占用比較久并發(fā)高了容易拖垮連接池。這里有一個(gè)很多新手容易踩的坑Redis鎖釋放時(shí)機(jī)。不能剛執(zhí)行完扣庫存就釋放鎖事務(wù)還沒提交的話另一個(gè)請求拿到鎖讀到的還是舊數(shù)據(jù)。我當(dāng)時(shí)把鎖的范圍包裹到整個(gè)事務(wù)方法外層用注解方式做了個(gè)簡單的鎖切面確保事務(wù)提交后才釋放鎖。代碼大致是下面這個(gè)結(jié)構(gòu)public OrderResult createOrder(Long scheduleId, Long userId) { String lockKey court:schedule:lock: scheduleId; boolean locked redisLock.tryLock(lockKey, 5, TimeUnit.SECONDS); if (!locked) { return OrderResult.fail(手速太快了換個(gè)時(shí)段試試); } try { return orderService.createOrder(scheduleId, userId); } finally { redisLock.unlock(lockKey); } }鎖的超時(shí)時(shí)間要結(jié)合實(shí)際業(yè)務(wù)耗時(shí)設(shè)置我用5秒是因?yàn)橄聠问聞?wù)里包含了微信支付預(yù)下單請求最慢也就兩三秒。設(shè)置太短會(huì)鎖失效太長會(huì)阻塞正常請求。這個(gè)參數(shù)沒有銀彈壓測之后再定最靠譜。2.3 微信登錄與會(huì)話保持code2Session那點(diǎn)事小程序端調(diào)用 wx.login() 拿到臨時(shí) code后端拿這個(gè) code 請求微信接口換取 openid 和 session_key。這里有幾個(gè)容易踩的坑我得說清楚。首先code 有效期只有5分鐘而且只能用一次。如果前端換了 code 后請求失敗重試就必須重新 wx.login()否則后端一查就報(bào)invalid code。其次session_key 不要自己存數(shù)據(jù)庫。它是微信側(cè)的會(huì)話密鑰有效期不固定而且官方建議敏感操作時(shí)才去解密。我們自己的系統(tǒng)要維護(hù)登錄態(tài)正確做法是拿到 openid 后查用戶表存在就當(dāng)老用戶處理不存在就自動(dòng)注冊然后自己生成一個(gè)業(yè)務(wù) token比如UUID或者JWT返回給小程序。小程序后續(xù)請求帶這個(gè) token后端從 token 解析出 userId。token 我建議存 Redis 并設(shè)置過期時(shí)間比如7天。每次請求通過攔截器校驗(yàn) token 有效性順便用 Redis 的過期機(jī)制實(shí)現(xiàn)無感續(xù)期。如果 token 固定不過期用戶體驗(yàn)是省事了但安全性差很多尤其是球館預(yù)約涉及支付訂單找回密碼這種功能雖然用不上但賬號被盜的后果還是得防。3. 小程序端的預(yù)約體驗(yàn)從場館列表到支付成功3.1 場館列表的加載更多分頁要這么做才不卡小程序端首頁是場館列表數(shù)據(jù)量不大但如果直接用 wx.request 一次性拉全量數(shù)據(jù)網(wǎng)絡(luò)差的時(shí)候會(huì)白屏很久而且用戶滑動(dòng)的時(shí)候沒有任何過渡。這里我用的是經(jīng)典的分頁加載 觸底加載更多模式。分頁參數(shù)我習(xí)慣用 pageNum 和 pageSize后端用 MyBatis-Plus 的 Page 插件一行代碼就能拿到分頁結(jié)果。小程序端維護(hù)三個(gè)核心變量pageNum、pageSize、hasMore。每次滾動(dòng)到底部觸發(fā) onReachBottompageNum 加一把新數(shù)據(jù) concat 到舊數(shù)據(jù)后面。hasMore 的判斷標(biāo)準(zhǔn)是本次返回的數(shù)據(jù)條數(shù)是否等于 pageSize如果小于說明沒有更多了。加載更多還有一個(gè)體驗(yàn)細(xì)節(jié)請求發(fā)出后要加一個(gè) loading 狀態(tài)標(biāo)記防止用戶快速滑動(dòng)觸發(fā)多次重復(fù)請求。用 boolean 變量 isLoading 控制請求開始置 true請求結(jié)束后置 false只有 isLoading 為 false 時(shí)才允許發(fā)起下一次請求。onReachBottom() { if (this.data.isLoading || !this.data.hasMore) return; this.setData({ isLoading: true, pageNum: this.data.pageNum 1 }); this.fetchVenueList(); }另外微信小程序在 onPullDownRefresh 下拉刷新時(shí)要調(diào)用 wx.stopPullDownRefresh()不然刷新動(dòng)畫會(huì)一直轉(zhuǎn)。這個(gè) API 很容易被忽略我在文檔里也單獨(dú)標(biāo)出來了。3.2 場地選擇與時(shí)段面板的狀態(tài)控制預(yù)約頁是整個(gè)小程序交互最復(fù)雜的部分用戶先選日期再選場地再選時(shí)段然后看到價(jià)格。這個(gè)頁面的核心是一場狀態(tài)聯(lián)動(dòng)。我采用了三段式布局頂部日期橫滑條中間場地 Tab底部時(shí)段網(wǎng)格。選日期的時(shí)候場地 Tab 和時(shí)段網(wǎng)格全部重置選場地的時(shí)候時(shí)段網(wǎng)格重新請求該場地該日期的排期數(shù)據(jù)。這里要注意不要每次切換都發(fā)請求最好做一層前端緩存比如按 courtId date 作為 key把排期數(shù)據(jù)存在內(nèi)存變量里切換回來直接讀緩存。考慮到小程序 setData 的性能瓶頸對于頻繁切換的交互緩存策略能明顯降低卡頓感。時(shí)段網(wǎng)格的每個(gè)格子根據(jù)排期數(shù)據(jù)有三種狀態(tài)可約、已被約、停售。已被約的格子置灰并顯示已約停售的顯示維護(hù)中只有可約狀態(tài)可點(diǎn)擊。點(diǎn)擊后高亮當(dāng)前選擇同時(shí)把價(jià)格顯示到頁面底部按鈕上。價(jià)格這里要由后端返回前端不要自己做計(jì)算否則后臺改價(jià)格規(guī)則后小程序端會(huì)顯示舊價(jià)格。日期橫滑條需要注意日期跨月和今天不能約過去的時(shí)段這兩個(gè)邊界。我的處理是日期列表由前端生成未來7天當(dāng)天之前不可選如果選的是今天后端查詢排期時(shí)會(huì)把開始時(shí)間早于當(dāng)前時(shí)間的時(shí)段直接標(biāo)記為不可約。這個(gè)規(guī)則前后端都要做后端是必須的前端是為了體驗(yàn)。3.3 支付前后的狀態(tài)機(jī)切換訂單狀態(tài)我用狀態(tài)機(jī)管理這一點(diǎn)強(qiáng)烈建議在文檔說明里畫一張狀態(tài)圖。整個(gè)狀態(tài)流轉(zhuǎn)是待支付 → 已支付 → 已核銷以及 待支付 → 已取消已支付 → 已退款。用戶在預(yù)約頁提交訂單后后端創(chuàng)建訂單并返回訂單號狀態(tài)是待支付同時(shí)調(diào)微信支付統(tǒng)一下單接口拿到支付參數(shù)返回給小程序。小程序收到后用 wx.requestPayment 拉起支付面板。這里有個(gè)非常容易出錯(cuò)的地方支付成功后要等回調(diào)更新狀態(tài)而不是前端直接跳轉(zhuǎn)。微信支付的回調(diào)是異步的前端 wx.requestPayment 的 success 只代表用戶輸入密碼完成了支付但后端還沒收到支付結(jié)果通知。如果這時(shí)候用戶立刻關(guān)掉小程序回調(diào)可能還沒到數(shù)據(jù)庫里的訂單還是待支付。穩(wěn)妥的做法是前端支付成功后就跳轉(zhuǎn)到支付結(jié)果確認(rèn)中頁面同時(shí)輪詢訂單狀態(tài)接口每1.5秒查一次直到訂單狀態(tài)變?yōu)橐阎Ц痘蛞淹丝畈盘厥醉摗]喸兊拇a要控制次數(shù)比如最多查10次超時(shí)后提示用戶支付結(jié)果確認(rèn)中請稍后在訂單列表查看。這個(gè)交互雖然笨一點(diǎn)但能避免大量支付了但訂單沒更新的客訴。支付回調(diào)接口一定要做冪等處理。微信可能因?yàn)榫W(wǎng)絡(luò)原因重復(fù)推送回調(diào)后端要根據(jù)訂單號判斷如果已經(jīng)是已支付就立刻返回 success不再做任何修改。我之前見過一個(gè)項(xiàng)目沒做冪等重復(fù)回調(diào)把訂單金額累加了兩遍對賬的時(shí)候賬都平不上。4. 管理后臺與訂單流轉(zhuǎn)核銷、退款與數(shù)據(jù)統(tǒng)計(jì)4.1 訂單列表與核銷流程給前臺一套高效的驗(yàn)票工具后臺管理端我用的是獨(dú)立的Web項(xiàng)目前端是Vue Element UI后端和預(yù)約小程序共用同一套Java服務(wù)。管理端的作用不只是看數(shù)據(jù)更重要的是處理線下場景。核銷是球館最常用的操作用戶到場后打開小程序出示核銷碼前臺在管理端輸入或者掃碼完成核銷。核銷碼我用了動(dòng)態(tài)碼方案訂單支付成功后生成一個(gè)6位數(shù)字碼過期時(shí)間是訂單日期的次日凌晨。前臺也可以按手機(jī)號查訂單再手動(dòng)核銷兩條路徑都做了兜底。核銷操作背后要校驗(yàn)幾件事訂單狀態(tài)必須是已支付、核銷碼和訂單號匹配、訂單日期是今天。這三條缺一條都不能放行。核銷完成后訂單狀態(tài)變?yōu)橐押虽N不可逆。我做了一個(gè)二次確認(rèn)彈窗前臺一旦誤操作可以少一點(diǎn)損失。這里實(shí)際踩過的坑是核銷碼里容易混入容易混淆的字符比如0和O、1和I生成時(shí)必須要剔除或者用純數(shù)字。管理端還支持手動(dòng)退款操作。退款調(diào)用微信支付退款接口退完更新訂單狀態(tài)。因?yàn)樯婕百Y金操作我加了一級管理員審批權(quán)限普通前臺只能發(fā)起退款申請老板在更高權(quán)限賬號上審批。審批通過后才真正調(diào)退款接口。這種設(shè)計(jì)不是為了復(fù)雜而是球館這種實(shí)體店退款糾紛特別容易發(fā)生在前臺操作不規(guī)范上權(quán)限拉開一層能省掉很多麻煩。4.2 運(yùn)營看板場館管理者真正關(guān)心的是這幾個(gè)數(shù)管理后臺首頁我做了一個(gè)簡單的數(shù)據(jù)看板不要搞花里胡哨的圖表最核心的是幾個(gè)指標(biāo)今日營收、今日訂單數(shù)、今日核銷數(shù)、未來7天預(yù)約量趨勢、各場地的預(yù)約熱度。未來7天預(yù)約量是我特意加的因?yàn)榍蝠^老板最關(guān)心的是今晚有沒有人訂。趨勢用簡單的柱狀圖展示預(yù)約熱度用場地的預(yù)約率排序這樣能直觀看到哪片場地最搶手為定價(jià)調(diào)整做參考。這里我剛開始犯過一個(gè)錯(cuò)把看板做得很重接了一套大屏用的數(shù)據(jù)可視化組件結(jié)果球館前臺電腦配置不高加載圖表卡到不行。后來全部換成輕量級組件甚至有一個(gè)版本直接輸出純HTML表格。我的建議是內(nèi)部管理工具的性能比視覺效果重要100倍優(yōu)先保證可用性再談美觀。5. 部署上線前的配置清單與實(shí)測踩坑記錄5.1 域名、HTTPS與小程序后臺配置漏一步都上線不了微信小程序的網(wǎng)絡(luò)請求有嚴(yán)格限制必須是 HTTPS 域名而且域名必須在小程序后臺配置到 request 合法域名里。我第一次上線就漏了這一步真機(jī)調(diào)試時(shí)所有接口都報(bào)url not in domain list排查了好久才反應(yīng)過來。配置清單大概是這樣的首先準(zhǔn)備一個(gè)備案過的域名解析到服務(wù)器。服務(wù)器上配好 Nginx 做反向代理同時(shí)給域名簽發(fā) HTTPS 證書我用的是免費(fèi)證書申請和續(xù)期都有自動(dòng)化工具。然后在小程序后臺把接口域名添加到 request 合法域名里。開發(fā)者工具里要關(guān)閉不校驗(yàn)合法域名這個(gè)選項(xiàng)再去測因?yàn)殚_著這個(gè)選項(xiàng)真機(jī)上就會(huì)出現(xiàn)上線前一切正常上線后全掛的尷尬局面。如果是本地開發(fā)可以用開發(fā)者工具自帶的不校驗(yàn)合法域名功能但記住這只是開發(fā)便利不是上線的依據(jù)。調(diào)試階段我想抓包看網(wǎng)絡(luò)請求細(xì)節(jié)用 Charles 代理看小程序和服務(wù)器之間的交互排查過幾個(gè)詭異的超時(shí)問題這個(gè)是看查問題的利器建議做小程序開發(fā)的朋友都提前熟悉。5.2 體驗(yàn)版分發(fā)與真機(jī)調(diào)試讓別人試用沒那么簡單開發(fā)完要發(fā)給球館老板試用不是直接把代碼發(fā)給他就行的。正確流程是在微信公眾平臺把代碼上傳設(shè)為體驗(yàn)版然后添加體驗(yàn)成員。這里有個(gè)坑體驗(yàn)成員必須是該小程序的開發(fā)者或者體驗(yàn)者而且需要在小程序后臺手動(dòng)添加。微信開發(fā)者工具里有個(gè)預(yù)覽功能可以生成二維碼但這個(gè)二維碼是臨時(shí)性的適合開發(fā)自測不適合長期給運(yùn)營人員用。體驗(yàn)版在真機(jī)上要用手機(jī)流量或者WiFi訪問一定確保手機(jī)和服務(wù)器網(wǎng)絡(luò)連通。我第一次給朋友發(fā)體驗(yàn)版他打開一片空白最后發(fā)現(xiàn)是服務(wù)器只允許內(nèi)網(wǎng)IP訪問手機(jī)用的是4G流量根本連不上。這個(gè)問題的排查思路很簡單用手機(jī)瀏覽器訪問一下接口域名能打開看到 JSON 就說明網(wǎng)絡(luò)通打不開就是防火墻或白名單問題。真機(jī)調(diào)試還有一個(gè)常見的適配問題頂部導(dǎo)航欄高度。iPhone X 系列和普通機(jī)型的狀態(tài)欄高度不一樣如果自定義導(dǎo)航欄要在 onLoad 里獲取 wx.getSystemInfoSync() 的狀態(tài)欄高度來做適配。用系統(tǒng)默認(rèn)導(dǎo)航欄就沒有這個(gè)問題但自定義體驗(yàn)好一點(diǎn)。我當(dāng)時(shí)的方案是頁面布局流一點(diǎn)導(dǎo)航欄直接用系統(tǒng)默認(rèn)的省掉一批適配問題把精力花在業(yè)務(wù)上。5.3 我的實(shí)測踩坑清單從上線到現(xiàn)在改過的這些問題有一說一上線前的測試再充分真實(shí)運(yùn)營起來還是會(huì)暴露問題。我在跑通這個(gè)項(xiàng)目前后最讓我印象深刻的幾個(gè)坑寫在這里給大家做個(gè)參考。第一個(gè)是時(shí)區(qū)問題。服務(wù)器是 UTC 時(shí)區(qū)數(shù)據(jù)庫存的是北京時(shí)間但定時(shí)任務(wù)生成排期時(shí)用的 LocalDate.now() 取的是服務(wù)器本地日期導(dǎo)致排期整體晚了一天。我后來統(tǒng)一在 Spring Boot 配置里指定了時(shí)區(qū)同時(shí)數(shù)據(jù)庫連接串里也加了 serverTimezoneAsia/Shanghai。這個(gè)問題如果不上線跑幾天根本發(fā)現(xiàn)不了但發(fā)現(xiàn)了就會(huì)很頭疼。第二個(gè)是支付回調(diào)的網(wǎng)絡(luò)抖動(dòng)。微信支付回調(diào)偶爾會(huì)有幾秒延遲如果用戶在小程序里一直等不到結(jié)果就會(huì)反復(fù)點(diǎn)支付按鈕。我后來在后端做了一個(gè)兜底待支付訂單超過30分鐘自動(dòng)關(guān)閉支付回調(diào)成功但訂單已關(guān)閉的情況自動(dòng)重新開啟訂單并進(jìn)入已支付狀態(tài)。這個(gè)邏輯起初沒想周全后來發(fā)現(xiàn)用戶等了太久不支付支付后訂單被關(guān)的情況后加上去的。第三個(gè)是小程序端 session 過期導(dǎo)致已登錄狀態(tài)集體失效。用戶在球館前臺用一臺手機(jī)核銷另一臺手機(jī)退出了登錄狀態(tài)業(yè)務(wù)數(shù)據(jù)倒沒亂但操作者會(huì)以為自己賬號丟了。我后來把 token 過期時(shí)間調(diào)長到14天同時(shí)在關(guān)鍵操作核銷、退款審批前用指紋或者密碼做二次身份確認(rèn)這樣賬號安全性和體驗(yàn)都照顧到了。第四個(gè)比較隱蔽批量接口的性能。訂單列表如果一次性查全量幾千條數(shù)據(jù)還能扛但跑到幾萬條就會(huì)明顯變慢。管理端列表我加了日期范圍篩選器和分頁同時(shí)給 order 表的 create_time 建了普通索引查詢從秒級降到毫秒級。這個(gè)優(yōu)化雖然基礎(chǔ)但對內(nèi)部工具來說提升非常直接。最后再分享一個(gè)實(shí)際運(yùn)營中的小技巧做這種預(yù)約系統(tǒng)代碼是一方面運(yùn)營策略也得跟上。我在上線之后發(fā)現(xiàn)球館最怕的是用戶預(yù)約了但沒來場地空著浪費(fèi)。后來我在管理端加了一個(gè)簡單的爽約統(tǒng)計(jì)連續(xù)爽約3次的用戶會(huì)收到提醒。這個(gè)小功能代碼量不大但球館老板反饋很好。如果你也打算做類似系統(tǒng)建議一開始就在用戶表里預(yù)留一個(gè) credit 或者 status 字段后期做運(yùn)營功能會(huì)少改很多表結(jié)構(gòu)。從項(xiàng)目規(guī)劃到上線我最大的體會(huì)是預(yù)約類系統(tǒng)的核心是業(yè)務(wù)模型的準(zhǔn)確性和數(shù)據(jù)一致性的把握這決定了系統(tǒng)能不能撐住真實(shí)場景而不是堆了多少花哨功能。編程技術(shù)和思維框架在項(xiàng)目演進(jìn)中的作用不只是把代碼寫出來而是讓你理解系統(tǒng)每一個(gè)狀態(tài)變化背后的業(yè)務(wù)含義。希望我的這個(gè)過程和踩坑經(jīng)驗(yàn)?zāi)軒湍阍谧约旱念A(yù)約系統(tǒng)項(xiàng)目里少走一段彎路。