療預(yù)約掛號系統(tǒng)全棧開發(fā)與部署實踐)
一個多月前我?guī)湍掣咝5男值茼椖拷M把一個“基于人臉識別的智慧醫(yī)療預(yù)約掛號平臺系統(tǒng)”從零到一完整跑通交付的時候包含源碼、配套論文、部署文檔和分步講解視頻。這套東西看起來就是典型的畢設(shè)級全棧項目但真正把它當(dāng)成一個能跑、能演示、能寫進(jìn)論文的完整系統(tǒng)來做涉及的點遠(yuǎn)不止“接一個人臉識別API”那么簡單。這篇就把我從需求拆解、數(shù)據(jù)庫設(shè)計、人臉核驗流程、并發(fā)占號到最后服務(wù)器部署踩過的坑全部梳理出來給正準(zhǔn)備做同類項目的同學(xué)一個可以直接參考的路線。這篇內(nèi)容適合三類人一是在做人臉識別/智慧醫(yī)療方向課程設(shè)計或畢設(shè)的學(xué)生二是想給中小診所、社區(qū)醫(yī)院做輕量化預(yù)約系統(tǒng)的開發(fā)人員三是對全棧項目從編碼到部署完整鏈路感興趣的同行。我會盡量把每一步為什么這么做、參數(shù)怎么定、坑在哪兒都說清楚不會只丟一個“能用就行”的糊弄方案。1. 項目核心思路拆解為什么是“人臉識別預(yù)約掛號”1.1 傳統(tǒng)預(yù)約掛號的痛點與我看到的改進(jìn)空間醫(yī)院掛號這個場景表面上是個業(yè)務(wù)流程問題本質(zhì)上是個身份確認(rèn)問題。傳統(tǒng)線上預(yù)約無非是“注冊賬號填手機號驗證碼選號源”這本沒什么問題但真正落到線下就露餡了患者手機上掛了號到院之后還要出示證件、報手機號、核對姓名窗口和導(dǎo)診臺的工作量一點沒降老人不會用手機子女幫忙掛號后老人到院被卡在身份核驗環(huán)節(jié)還有號販子批量注冊賬號搶號再去倒賣后臺很難識別。人臉識別的價值恰恰就在這里。把人臉唯一性和預(yù)約單綁定之后患者可以在家完成建檔到院之后在自助機或者診區(qū)簽到終端前掃一下臉身份核驗、預(yù)約確認(rèn)、就診簽到一步到位。對于不會操作智能手機的老人監(jiān)護(hù)人代掛號后可憑患者本人面部信息直接到院核驗體驗比翻證件找取號二維碼順滑得多。1.2 技術(shù)選型為什么優(yōu)先選人臉識別方案選人臉識別而不是繼續(xù)用驗證碼、短信二次驗證核心是解決兩個矛盾一是“線上確認(rèn)了身份線下沒人能證明是他本人”二是“號源有限但搶號的人不一定是看病的人”。從技術(shù)角度講人臉識別用在預(yù)約流程里不是要做得像安防系統(tǒng)那么極端我們要求的是“1:1核驗”和“1:N檢索”兩種能力。1:1核驗用于患者到院時用現(xiàn)場抓拍的照片跟預(yù)約建檔時留存的人臉底圖做比對判斷“來的人是不是預(yù)約單上這個人”。1:N檢索用于防止重復(fù)建檔、黑名單攔截比如某身份證號或某張臉在系統(tǒng)里已經(jīng)存在爽約記錄。方案選型上我建議優(yōu)先選用本地化部署的人臉識別引擎而不是每次調(diào)用云端接口。原因很現(xiàn)實醫(yī)院內(nèi)網(wǎng)和公網(wǎng)之間往往有復(fù)雜的網(wǎng)絡(luò)安全邊界患者人臉照片屬于敏感個人生物信息數(shù)據(jù)出境和數(shù)據(jù)存儲都有合規(guī)壓力本地化部署讓人臉特征向量只在內(nèi)網(wǎng)流轉(zhuǎn)后續(xù)寫論文、應(yīng)付答辯也更好解釋安全設(shè)計。2. 系統(tǒng)整體設(shè)計與數(shù)據(jù)建模2.1 功能模塊規(guī)劃從預(yù)約到就診的閉環(huán)我設(shè)計的系統(tǒng)按角色拆成三類端患者端小程序/公眾號H5、醫(yī)生端工作臺、醫(yī)院管理后臺?;颊叨说暮诵逆溌肥亲?→ 實名建檔 → 人臉信息采集 → 科室/醫(yī)生選擇 → 號源鎖定 → 支付/確認(rèn) → 預(yù)約單生成 → 到院刷臉簽到 → 查看候診隊列。這里“人臉信息采集”不要求用戶上傳照片而是通過小程序調(diào)用手機攝像頭拍攝一段實時視頻流后端抽幀做人臉質(zhì)量檢測質(zhì)量合格后提取特征向量入庫。這樣能避免“端上照片被翻拍、替身照”的問題。醫(yī)生端和管理后臺相對常規(guī)醫(yī)生端查看到院簽到順序、叫號、寫診后記錄后臺管理負(fù)責(zé)科室排班、號源池配置、黑名單管理、異常預(yù)約處理。2.2 數(shù)據(jù)庫表設(shè)計要點數(shù)據(jù)庫設(shè)計是整個系統(tǒng)的地基。我采用MySQL 8.0核心表有六張。每張表都要留出擴展字段比如“特征版本號”因為人臉模型升級后特征維度可能變化需要區(qū)分新舊特征。用戶主表uid、手機號、密碼摘要、角色標(biāo)識、狀態(tài)。密碼摘要用BCrypt不用明文?;颊邫n案表檔案id、uid、真實姓名、證件類型、證件號、出生日期、緊急聯(lián)系人。證件號要做加密存儲后端服務(wù)里統(tǒng)一加解密不能直接select出明文。人臉特征表特征id、檔案id、特征向量用JSON或BLOB存、特征版本、采集時間、活體檢測得分。特征向量是核心數(shù)據(jù)建議單獨建表因為人臉底庫的讀取策略和用戶基本資料不一樣分離存儲便于后續(xù)把特征庫遷到向量數(shù)據(jù)庫或獨立人臉服務(wù)節(jié)點。醫(yī)生排班表排班id、醫(yī)生id、科室id、出診日期、午別上午/下午、號源總量、已占號量、版本號。版本號是樂觀鎖用的下面講并發(fā)控制時會具體說。預(yù)約單表預(yù)約單號、檔案id、排班id、就診狀態(tài)、預(yù)約時間、簽到時間、候診序號、支付狀態(tài)。狀態(tài)機建議設(shè)計為待支付 → 已支付/待就診 → 已簽到 → 就診中 → 已完成以及獨立的已退號、爽約態(tài)。黑名單表檔案id、觸發(fā)規(guī)則、鎖定截止時間。爽約和惡意占號都會寫進(jìn)這里。2.3 人臉識別模塊的接入思路人臉識別引擎獨立部署成一個Python服務(wù)對外提供三個接口人臉質(zhì)量檢測、人臉特征提取、人臉特征比對。后端主程序是Java技術(shù)棧的話通過HTTP調(diào)用這個Python服務(wù)即可。技術(shù)棧隔離的好處是人臉模型推理依賴的OpenCV、PyTorch等庫不污染Java服務(wù)的環(huán)境后續(xù)單獨升級模型也不影響業(yè)務(wù)主程序。質(zhì)量檢測接口接收圖片后返回人臉檢測框坐標(biāo)、模糊度、遮擋分?jǐn)?shù)、角度。只有角度偏轉(zhuǎn)小于15度、模糊度低于閾值、遮擋面積小于設(shè)定值時才允許進(jìn)入下一步。特征提取接口輸出512維浮點向量特征比對接口計算兩個向量之間的余弦相似度超過0.62判定為同一人。這個閾值是我在測試集上調(diào)出來的太嚴(yán)會導(dǎo)致老人、兒童識別率低太松則可能誤放行。3. 核心流程實現(xiàn)預(yù)約、核驗、簽到3.1 首次建檔與人臉信息登記流程建檔這個動作最容易做得虎頭蛇尾。很多同學(xué)習(xí)慣讓用戶填完基本資料就算建檔完成人臉信息另找時間補錄結(jié)果就是到院刷臉時發(fā)現(xiàn)底庫是空的很尷尬。我的設(shè)計是把人臉采集并進(jìn)建檔步驟里前端先拍證件OCR自動識別姓名和證件號再通過活體檢測拍一段短視頻后端從視頻中抽三幀分別做人臉質(zhì)量檢測取質(zhì)量分最高的一幀做特征提取。為什么是三幀不是一幀因為單幀可能遇到閉眼、低頭、逆光三幀取質(zhì)量最優(yōu)能顯著降低建檔階段的人臉照片廢片率。活體檢測這步不要省。如果沒有動作活體檢測別人拿一張患者照片也能完成建檔。我當(dāng)時用了一個極簡方案屏幕隨機顯示“點頭/張嘴/左轉(zhuǎn)頭”指令用戶完成動作后采集6到8幀圖像序列送到Python服務(wù)做關(guān)鍵點軌跡分析軌跡符合指令動作才視為通過。這個方案實現(xiàn)成本不高但答辯時被問“如何防止照片攻擊”就有得說了。3.2 預(yù)約掛號業(yè)務(wù)流號源鎖定與支付預(yù)約掛號最敏感的是號源并發(fā)問題。大家寫死“先查號源再插入預(yù)訂單”并發(fā)一高就會出超賣。我們用樂觀鎖解決醫(yī)生排班表上保留一個版本號字段預(yù)約時執(zhí)行的SQL類似這樣UPDATE doctor_schedule SET occupied occupied 1, version version 1 WHERE id ? AND occupied total AND version ?;如果返回影響行數(shù)為0說明號源不足或版本號已變化直接提示“號源被搶完或已被占用”。這樣避免引入分布式鎖復(fù)雜度對畢設(shè)和中小醫(yī)院場景完全夠用。號源鎖定成功后生成預(yù)約單并開啟15分鐘的支付倒計時。這里有個隱藏問題用戶鎖號后不去支付號源會一直被占著。解決辦法是啟動一個定時任務(wù)每分鐘掃描狀態(tài)為“待支付”且創(chuàng)建時間超過15分鐘的預(yù)訂單自動釋放號源。定時任務(wù)用Spring的Scheduled即可固定延遲2分鐘避免整點批處理打崩數(shù)據(jù)庫。3.3 到院簽到與就診核驗人臉比對的實時交互到院簽到是整個系統(tǒng)的高光時刻。我用兩套終端自助簽到大屏和診區(qū)門口的小型簽到平板?;颊唿c擊屏幕上的“刷臉簽到”攝像頭抓拍一張照片后端執(zhí)行三步第一步人臉檢測與質(zhì)量過濾模糊、逆光的照片直接提示重新拍攝避免無效比對浪費算力。第二步取當(dāng)前科室當(dāng)前時段的所有預(yù)約單關(guān)聯(lián)人臉特征底庫做1:N檢索因為門診單位時間內(nèi)到場人數(shù)有限可以把檢索范圍控制在“未來2小時預(yù)約當(dāng)前科室患者”這個集合內(nèi)比對速度會非???。第三步比對命中后更新預(yù)約單狀態(tài)為已簽到生成候診序號同時推送給醫(yī)生端叫號大屏。這里有個細(xì)節(jié)簽到不能只做一次人臉比對就算完還應(yīng)該校驗預(yù)約時間。我設(shè)定患者只能在預(yù)約就診時間段前30分鐘和后60分鐘內(nèi)完成簽到提前太多提示“尚未到簽到時段”遲到超過1小時自動標(biāo)記為遲到由分診臺人工決定是否順延。這個規(guī)則一開始沒做結(jié)果有人凌晨三點就到醫(yī)院刷臉把值班系統(tǒng)搞出了奇怪的提前簽到數(shù)據(jù)。3.4 后臺管理號源池、退號與爽約處理后臺管理模塊容易被忽略但恰恰是寫論文時最能體現(xiàn)“系統(tǒng)完整性”的部分。號源池配置支持按排班批量生成一周的號源每日號源量 單人次平均就診時長 / 出診總時長比如一個上午門診4小時均次就診8分鐘那號源量約30個。這個計算邏輯要能配置因為不同科室均次時長差異很大皮膚科和心內(nèi)科完全不是一個量級。退號流程我設(shè)置了兩個關(guān)鍵邊界就診前24小時以上可在線自助退號金額原路返回24小時內(nèi)退號需管理員審核防止號販子臨診前大量釋放號源擾亂排班。爽約規(guī)則是“30天內(nèi)爽約2次凍結(jié)線上預(yù)約權(quán)限7天”這個規(guī)則能有效壓制測試階段發(fā)現(xiàn)的人力刷號問題。4. 部署與上線注意點4.1 本地開發(fā)環(huán)境的快速搭建本地跑通前后端協(xié)同開發(fā)我建議用Docker Compose一鍵起MySQL、Redis、Java后端、Python人臉服務(wù)、前端Nginx靜態(tài)資源。version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: smart_clinic ports: - 3306:3306 volumes: - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql redis: image: redis:7-alpine ports: - 6379:6379 face-engine: build: ./face-engine ports: - 8000:8000 backend: build: ./backend ports: - 8080:8080 depends_on: - mysql - redis web: image: nginx:alpine ports: - 80:80人臉服務(wù)這個鏡像會比較大因為要裝PyTorch和OpenCV。我第一次build直接卡了十幾分鐘后來改成在Dockerfile里固定國內(nèi)鏡像源、分階段安裝依賴才把構(gòu)建時間壓到5分鐘以內(nèi)。本地調(diào)試時建議直接用虛擬環(huán)境跑Python服務(wù)不要每次改代碼都走完整build流程。4.2 服務(wù)器部署前后端分離與模型加載服務(wù)器部署我選擇一臺4核8G的云主機操作系統(tǒng)Ubuntu 22.04。分層策略是最外層Nginx托管前端Vue打包產(chǎn)物反向代理/api路徑到Java服務(wù)端口8080反向代理/face路徑到Python人臉服務(wù)端口8000。這樣前端只需要認(rèn)識一個域名不需要感知后端服務(wù)拆分。人臉模型加載有個大坑PyTorch加載預(yù)訓(xùn)練模型默認(rèn)需要較多內(nèi)存4G內(nèi)存的服務(wù)器上跑起來非常吃力。我的處理方式是在Python服務(wù)啟動時延遲加載模型首次請求觸發(fā)加載加載完成后駐留顯存/內(nèi)存。服務(wù)啟動后體檢一下人臉接口延遲單次特征提取應(yīng)控制在150毫秒以內(nèi)比對控制在30毫秒以內(nèi)。如果你實測明顯高于這個值先看CPU是否滿核跑再看有沒有把圖片base64編碼后傳圖片壓縮到720px寬度能大幅降低網(wǎng)絡(luò)傳輸和推理耗時。4.3 性能與安全方面的取舍人臉圖片這種生物數(shù)據(jù)我做了兩層保護(hù)接口層必須攜帶一次性臨時憑證憑證有效期為10分鐘防止截圖后離線重放存儲層對人臉原始圖單獨加密保存業(yè)務(wù)庫只存特征向量。特征向量本身是數(shù)學(xué)量無法直接還原成照片泄露風(fēng)險相對可控。數(shù)據(jù)庫方面連接池建議設(shè)置最小5、最大20超出等待時間設(shè)置為3秒。這是由于人臉比對接口慢時Java服務(wù)的數(shù)據(jù)庫查詢會排隊堆積連接池過大會把MySQL線程數(shù)打滿我實際測試20個連接足夠支撐門診高峰的并發(fā)。5. 常見問題排查與避坑實錄5.1 刷臉失敗率高先查這三個環(huán)節(jié)刷臉失敗一般不是模型不夠好而是現(xiàn)場環(huán)境影響。首先查攝像頭采集分辨率很多人臉服務(wù)默認(rèn)丟入模型前會壓縮圖片如果前端采集幀本身只有480p人臉關(guān)鍵點會缺失識別率驟降。我的經(jīng)驗是前端采集至少1280x720抓拍后將人臉區(qū)域裁剪放大后再送后端。其次是光線醫(yī)院診區(qū)燈光經(jīng)常偏冷偏暗。我在自助機屏幕上方加了一個補光燈板實測失敗率從12%降到3%以下。最后是用戶姿勢簽到平板上我畫了“人臉放框內(nèi)、平視攝像頭”的引導(dǎo)動畫這一步非常有效。測試環(huán)境很多失敗是下巴過度抬起或側(cè)臉過大導(dǎo)致質(zhì)量檢測接口應(yīng)該直接攔截并提示用戶調(diào)整而不是把低質(zhì)量照片硬送比對。5.2 號源超賣問題別用查再插寫業(yè)務(wù)邏輯剛開始最典型的問題是先select當(dāng)前已占號量判斷小于總量然后insert預(yù)訂單。兩個請求同時查到已占號量一樣就會雙雙插入成功。我改成前面那個樂觀鎖更新后測試腳本用200個并發(fā)搶30個號源最終訂單數(shù)正好30無一超賣。補充一個排查技巧如果發(fā)現(xiàn)更新語句影響行數(shù)總是0檢查一下排班id是否誤用了另一個字段作為條件。5.3 接口響應(yīng)慢定位瓶頸要分層看一次完整的預(yù)約請求鏈路里包含前端→Java→MySQL/Redis→Python人臉服務(wù)鏈路長哪個環(huán)節(jié)慢了都會拖垮體驗。我推薦在Java服務(wù)里加一個簡單的耗時切面把每次外部調(diào)用時間打出來。快速判斷依據(jù)是若人臉特征提取接口耗時長優(yōu)先在Python服務(wù)里開多線程/多進(jìn)程來處理并發(fā)請求多時單線程服務(wù)會排隊若MySQL查詢慢優(yōu)先檢查預(yù)約單表有沒有對“檔案id預(yù)約日期”建聯(lián)合索引。我碰到過一次詭異的現(xiàn)象并發(fā)一高Java接口整體卡頓MySQL的CPU不高Redis卻撐了一下就崩。排查下來是登錄態(tài)session直接存Rediskey設(shè)置不規(guī)范一個用戶反復(fù)登錄產(chǎn)生大量過期key加上懶刪除策略的隨機抽查導(dǎo)致高并發(fā)時阻塞。把session過期時間統(tǒng)一為2小時并且改成主動過期刪除后解決。5.4 照片翻拍與底庫質(zhì)量不匹配注意模型版本一致性人臉特征比對的鐵律是特征提取和特征比對必須使用同一版本模型。建檔時用模型A提取的特征向量線下簽到用模型B做比對即使兩個模型精度都很高跨模型的余弦相似度也會整體偏低。我經(jīng)歷過一次升級模型后老用戶全部被拒的嚴(yán)重事故后來方案是特征向量中都帶feature_version字段比對時如果發(fā)現(xiàn)版本不一致先拒絕并轉(zhuǎn)人工核驗而不是用新模型去硬比舊向量。另一個容易被忽視的坑是證件照底圖與生活照的差異。某個患者身份證照是十年前拍的現(xiàn)場人臉識別怎么都比不過閾值。后來我在建檔時增加“可另外錄入一張現(xiàn)場真實照片”的選項以現(xiàn)場照為主特征、證件照為輔助特征兩條向量都存下來比對時取最高分。這算是一個低成本高收益的改進(jìn)。6. 從項目到產(chǎn)品的延展思考6.1 單院區(qū)版本邁向多院區(qū)版本的數(shù)據(jù)模型變化如果這套系統(tǒng)要從單體醫(yī)院擴展到醫(yī)聯(lián)體第一個要改的是身份主索引。不同醫(yī)院之間患者檔案需要統(tǒng)一但人臉特征向量未必能跨院共享因為各院區(qū)的采集設(shè)備型號、光線條件、模型版本都可能不一致。穩(wěn)妥做法是建立“全局患者ID 院區(qū)特征分庫”的結(jié)構(gòu)預(yù)約單要帶上所屬院區(qū)字段簽到終端只能檢索當(dāng)前院區(qū)特征庫。這里還有一個業(yè)務(wù)規(guī)則問題跨院區(qū)預(yù)約需要共享號源池嗎我的建議是初期各院區(qū)獨立號源池后臺管理端做匯總視圖即可。否則一個患者在同一時間段內(nèi)被兩個院區(qū)誤傷鎖號容易引發(fā)醫(yī)院內(nèi)部的號源分配糾紛。6.2 人臉服務(wù)的高可用演進(jìn)單節(jié)點的人臉服務(wù)在實際門診場景下風(fēng)險不小。有一次我用Gunicorn默認(rèn)worker跑Python服務(wù)一個模型的推理請求阻塞后續(xù)請求全部排隊門診高峰期簽到平板直接轉(zhuǎn)圈。至少應(yīng)該做到人臉服務(wù)部署雙副本Nginx做輪詢負(fù)載均衡比對接口超時設(shè)為2秒超時后立即降級為“人工核驗通道”而不是讓患者在終端前干等。將來有條件的話可以把特征向量從MySQL搬到向量數(shù)據(jù)庫1:N檢索性能能提升一個量級。但對于預(yù)約掛號場景患者集合是可控的提前按科室和時段圈定檢索范圍比換數(shù)據(jù)庫更加立竿見影。6.3 演示與講解視頻的制作心得最后分享一個交付層面的經(jīng)驗。源碼之外的那份講解視頻一定不要照著部署文檔念而是講三個故事第一個故事是“患者從家到簽到臺全程無感通行”把業(yè)務(wù)流串起來第二個故事是“200個并發(fā)搶30個號源不超賣”把技術(shù)難點亮出來第三個故事是“模型升級導(dǎo)致老用戶全被拒”把工程教訓(xùn)說出來。面試官和答辯老師看到這三個場景就知道你不是在堆功能而是在認(rèn)真解決過實際問題。另外部署文檔最好按“全新虛擬機從零開始”的視角寫裝什么依賴、改哪些配置、起什么服務(wù)、看什么日志確認(rèn)成功每一行命令都給出預(yù)期輸出。我當(dāng)年踩過的最冤枉的坑就是文檔里漏寫了人臉服務(wù)需要先安裝特定版本的libgl1導(dǎo)致評審老師換了個機器部署時一連串環(huán)境報錯。這套系統(tǒng)真要做扎實工程量不小但它覆蓋了一個全棧項目幾乎所有的核心訓(xùn)練點業(yè)務(wù)建模、并發(fā)控制、生物識別接入、前后端分離部署、異常降級設(shè)計。做完這一套你手里就不只是“一個畢設(shè)”而是可以大大方方寫進(jìn)簡歷的“一整個項目”。如果時間允許建議你把黑名單規(guī)則、號源池配置、日志審計再補細(xì)一點這套東西的延展空間還很大。