藥信息管理系統(tǒng)數據庫設計:從三范式建模到事務與索引優(yōu)化實踐)
簡介這是一份面向高校數據庫課程設計場景的醫(yī)藥信息管理系統(tǒng)完整項目包覆蓋藥品、員工、客戶、供應商等基礎信息維護并實現進貨、庫房、銷售、財務統(tǒng)計四大業(yè)務模塊適合正在做數據庫課設或畢設、需要可直接運行參考系統(tǒng)的學生。資源為zip壓縮包共335個文件約4.05MB體積緊湊便于快速部署其中含150個gif演示截圖、54個java源碼文件以及html/js/css前端頁面、sql數據庫腳本、Maven配置和項目說明文件目錄結構清晰便于導入IDE后整體跑通。目前已有71人學習下載。包內完整代碼和界面素材可直接用于理解系統(tǒng)分層與數據庫表設計也可在此基礎上擴展訂單、庫存預警等功能能有效節(jié)省前后端搭建時間是完成醫(yī)藥管理類課設的高價值參考資料。1. 數據庫課設醫(yī)藥信息管理系統(tǒng)一張?zhí)幏嚼镉卸嗌贁祿軘祿煺n程設計只要題目一帶“醫(yī)藥信息管理系統(tǒng)”很多同學第一反應就是“又是藥品增刪改查”。但真要在答辯時把一張?zhí)幏街v清楚背后其實是三庫分離的底子藥品基礎數據、供應商與采購流水、客戶處方與銷售庫存。這個系統(tǒng)最自然的落點是用 MySQL 或 SQL Server 建出符合第三范式的十幾張表再用事務把“開處方→扣庫存→記流水”這一串動作包起來讓數據要么全部生效、要么全部回滾。適合誰做MySQL、SQL Server 這門課剛學完、想在課設里同時體現“設計能力 代碼落地”的同學也適合畢業(yè)設計想低成本先搭一個能演示的醫(yī)療業(yè)務底座。讀完這篇你能照著一套可復現的建表 SQL 和 JavaJDBC調用方式把數據庫課設從“畫 ER 圖”一路做到“能開機演示”。2. 先設計再建表ER 模型、范式與 8 張核心表課設答辯時最容易被追問的不是“你怎么寫代碼”而是“為什么這么建表”。所以建表之前必須先把業(yè)務實體抽出來再按范式和后續(xù)查詢習慣去拆表真正落地成 SQL。2.1 從業(yè)務流程抽實體藥品、供應商、庫存和處方誰先誰后醫(yī)藥信息管理系統(tǒng)最常見的業(yè)務閉環(huán)是采購入庫 → 庫存管理 → 銷售出庫。圍繞這個閉環(huán)實體有七八個是正常的少了會被老師說“太簡陋”多了又容易掉進過度設計。我一般按四條主線去抽藥品主線藥品信息、藥品分類、藥品規(guī)格。采購主線供應商、采購訂單、采購明細、入庫記錄。銷售主線客戶或患者、處方/銷售單、銷售明細、退款記錄。庫存與財務主線庫存表、流水表、盤點記錄。這里最關鍵的設計決策是藥品信息表只放“通用屬性”把價格、批號、效期放到獨立的表里。很多課設把藥品表搞成一個大寬表加上drug_name、price、stock、expire_date一大堆——一修改就冗余。更穩(wěn)的做法是拆出drug_stock和drug_batch。批號獨立成表還有一個實際好處藥監(jiān)場景下同一藥品多個批號、不同進價、不同效期按“一藥一批”做管理才說得通。ER 圖里把這兩個多值字段拆出去天然符合第二范式和第三范式。2.2 三段式建庫建庫、建表、插種子數據下面這組 SQL 以 MySQL 8.0 為準SQL Server 2019 只要把AUTO_INCREMENT換成IDENTITY(1,1)、反引號去掉就能平移過去。先建庫再建核心表最后灌種子數據用于演示。-- 建庫字符集用 utf8mb4避免中文和生僻字亂碼 CREATE DATABASE IF NOT EXISTS pharma_sys DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE pharma_sys; -- 1. 藥品分類表樹形結構parent_id 指向自身主鍵 CREATE TABLE drug_category ( id INT AUTO_INCREMENT PRIMARY KEY, parent_id INT DEFAULT 0 COMMENT 父分類 id0 表示頂級, category_name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ); -- 2. 藥品信息表只有“不變”的基礎屬性 CREATE TABLE drug_info ( id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL, drug_code VARCHAR(20) NOT NULL UNIQUE COMMENT 藥品編碼作為業(yè)務唯一鍵, drug_name VARCHAR(100) NOT NULL, spec VARCHAR(50) COMMENT 規(guī)格如 5mg*20片, unit VARCHAR(10) DEFAULT 盒, manufacturer VARCHAR(100), approval_no VARCHAR(30) COMMENT 批準文號, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 3. 供應商表 CREATE TABLE supplier ( id INT AUTO_INCREMENT PRIMARY KEY, supplier_name VARCHAR(100) NOT NULL, contact_person VARCHAR(20), phone VARCHAR(20), address VARCHAR(200) ); -- 4. 藥品庫存表數量只在這里維護 CREATE TABLE drug_stock ( drug_id INT PRIMARY KEY COMMENT 與 drug_info.id 一一對應, batch_no VARCHAR(30) COMMENT 批號, expire_date DATE COMMENT 效期, quantity DECIMAL(12,3) DEFAULT 0 COMMENT 庫存數量支持最小拆零單位, purchase_price DECIMAL(10,3), sale_price DECIMAL(10,2) );字段說明里要特別注意decimal而不是float金額和數量在課設里看起來都是小數但float是近似存儲1.4 變成 1.399999999 的新聞年年有。價格用DECIMAL(10,2)庫存用DECIMAL(12,3)是為了兼容“按克賣”的拆零藥品演示時也能講出理由。drug_code單獨加唯一索引避免業(yè)務里拿自增主鍵到處傳——這是給藥品、供應商、處方三個模塊共用的“主鍵外露”教訓。銷售側的兩張表單獨再建因為它們承擔“流水”職責不適合和基礎表混在一起-- 5. 銷售主表處方/銷售單頭 CREATE TABLE sales_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(20) NOT NULL UNIQUE COMMENT 單號演示時用時間戳生成, customer_name VARCHAR(50), total_amount DECIMAL(10,2) DEFAULT 0, sale_status TINYINT DEFAULT 0 COMMENT 0 待付款 1 已付款 2 已退款, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 6. 銷售明細表一個單頭對應多行 CREATE TABLE sales_order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, drug_id INT NOT NULL, drug_name VARCHAR(100) COMMENT 冗余字段防止藥品改名后歷史單看不清, quantity DECIMAL(10,3) NOT NULL, price DECIMAL(10,2) NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT quantity * price ); -- 7. 入庫記錄表供應商、采購、效期一起落這里 CREATE TABLE stock_in_record ( id INT AUTO_INCREMENT PRIMARY KEY, drug_id INT NOT NULL, supplier_id INT, batch_no VARCHAR(30), quantity DECIMAL(10,3) NOT NULL, purchase_price DECIMAL(10,3), operate_user VARCHAR(20), in_time DATETIME DEFAULT CURRENT_TIMESTAMP );sales_order_item里冗余drug_name看似違反范式實際上是有意為之歷史銷售單里的藥品名必須“定格”否則藥品改名之后報表上全是新名字說不清楚當時賣的是什么。這個設計點答辯時主動講老師會認為你理解“范式是為了查詢服務不是教條”。2.3 外鍵與索引什么時候該設什么時候不該設建表之后就是外鍵和索引的選擇題。外鍵在課設里建議設因為sales_order_item.order_id、drug_stock.drug_id這類關系是強約束寫不存在的藥品編碼直接報錯演示時也能體現“數據庫自身在兜底”。但要注意stock_in_record.supplier_id這類弱關聯我一般不加外鍵加一個普通索引就可以因為供應商改不修改并不影響入庫流水加了外鍵反而會讓DELETE FROM supplier頻頻失敗。索引是數據庫優(yōu)化里最容易講出內容的點。sales_order的order_no必須唯一索引sales_order_item的order_id加普通索引stock_in_record的drug_id batch_no加聯合索引。真正要克制的是不要在每列都加索引醫(yī)藥系統(tǒng)里頻繁查詢的是“按單號查明細”“按藥品查庫存”“按供應商查流水”這三條覆蓋到了就夠用。3. 把增刪改查做“穩(wěn)”事務、存儲過程與連接池配置表建好只是地基。很多數據庫課設的死穴是業(yè)務代碼里先查庫存再改庫存結果兩個窗口同時下單庫存變成了負數。所以這一章要解決的核心問題是“并發(fā)和一致怎么在自己手里控制住”落到三個點上事務邊界、庫存扣減的原子更新、連接池參數。3.1 并發(fā)扣庫存為什么必須用行鎖而不是先查后改“先查數量是否夠 → 夠就 UPDATE”是新手最常見的寫法也是高并發(fā)翻車的源頭。兩個會話同時讀到庫存還剩 10各自都判斷“夠”各自減去 1庫存最終變成 9但實際賣了 2 件。數據庫里的行鎖解決的是“寫之間互斥”不是“讀與寫之間互斥”所以靠“先查后改”根本鎖不住。解法是把判斷和扣減合成一條 SQL利用行鎖的原子性-- 扣庫存一次 UPDATE 完成“查詢 校驗 修改” UPDATE drug_stock SET quantity quantity - 1 WHERE drug_id 1 AND quantity 1; -- 影響行數 1扣減成功 -- 影響行數 0庫存不足回滾整個事務這條 SQL 在任何隔離級別下都不會超賣quantity 1條件讓多余的扣減直接匹配不到行而 UPDATE 本身會對命中的行加排他鎖。再配合下面的庫存流水表記錄就能做到“賬實相符”。-- 庫存流水每次增減都記一行作為審計依據 CREATE TABLE stock_flow ( id INT AUTO_INCREMENT PRIMARY KEY, drug_id INT NOT NULL, change_type TINYINT COMMENT 1 入庫 2 銷售出庫 3 盤點調整, change_quantity DECIMAL(10,3), before_quantity DECIMAL(10,3), after_quantity DECIMAL(10,3), flow_time DATETIME DEFAULT CURRENT_TIMESTAMP );參數說明change_type用TINYINT加注釋存數字不存中文數據量上來之后查詢和統(tǒng)計都方便。before_quantity和after_quantity留雙份快照是為了報表里直接算出“變化前后”避免再去關聯庫存表。血淚經驗如果庫存流水只記變化量不記前后值年終盤點對賬時基本要重跑一遍全量數據才能定位差異。3.2 用存儲過程包住“銷售主表 明細 庫存”三件事多表寫入的正確姿勢是放在一個事務里。這里給一個 MySQL 存儲過程的寫法它把三步操作包成單次調用插入銷售主表、批量插入明細、逐條扣庫存。任何一步失敗全部回滾。DELIMITER $$ CREATE PROCEDURE sp_create_sales_order( IN p_order_no VARCHAR(20), IN p_customer_name VARCHAR(50), IN p_items JSON -- 明細以 JSON 數組傳入 ) BEGIN DECLARE v_order_id INT; DECLARE v_drug_id INT; DECLARE v_qty DECIMAL(10,3); DECLARE v_rows INT DEFAULT 0; DECLARE v_i INT DEFAULT 0; START TRANSACTION; -- 1. 寫銷售主表 INSERT INTO sales_order(order_no, customer_name, sale_status) VALUES(p_order_no, p_customer_name, 1); SET v_order_id LAST_INSERT_ID(); -- 2. 解析 JSON 數組逐條寫明細并扣庫存 SET v_rows JSON_LENGTH(p_items); WHILE v_i v_rows DO SET v_drug_id JSON_UNQUOTE(JSON_EXTRACT(p_items, CONCAT($[, v_i, ].drugId))); SET v_qty JSON_UNQUOTE(JSON_EXTRACT(p_items, CONCAT($[, v_i, ].quantity))); INSERT INTO sales_order_item(order_id, drug_id, drug_name, quantity, price, amount) SELECT v_order_id, id, drug_name, v_qty, sale_price, v_qty * sale_price FROM drug_info JOIN drug_stock ON drug_info.id drug_stock.drug_id WHERE drug_info.id v_drug_id; -- 上面 INSERT 影響行數為 0 說明藥品不存在直接回滾 IF ROW_COUNT() 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 藥品不存在或已停售; END IF; -- 原子扣庫存影響行數為 0 說明庫存不足 UPDATE drug_stock SET quantity quantity - v_qty WHERE drug_id v_drug_id AND quantity v_qty; IF ROW_COUNT() 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 庫存不足; END IF; SET v_i v_i 1; END WHILE; -- 3. 更新主表總金額 UPDATE sales_order SET total_amount (SELECT SUM(amount) FROM sales_order_item WHERE order_id v_order_id) WHERE id v_order_id; COMMIT; END$$ DELIMITER ;這段代碼的核心不是 JSON 解析而是事務邊界的切割。START TRANSACTION之后所有寫操作共享同一個事務中間任何ROLLBACK都會把已插入的明細撤干凈。參數里p_items用 JSON 僅僅是為了演示一個完整調用鏈如果你課設的數據庫是 SQL Server 2019可以把 JSON 換成臨時表或表值參數思路完全一致。調用方Java 的 JDBC只需要CallableStatement注冊這三個參數p_order_no、p_customer_name、p_items。注意 JSON 字符串里不能用單引號包鍵名MySQL 的 JSON 解析只認雙引號這段在聯調時經常被卡一下。3.3 連接池參數與 UTF-8Navicat 導出腳本常見的被埋參數課設里數據庫連接最常出問題的是兩處JDBC URL 忘了加characterEncoding以及連接池線程配得太小導致“假死”。下面是一份實際可用的druid.properties配置及注釋# Druid 連接池基礎參數 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/pharma_sys?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密碼 # 初始連接數 / 最小空閑 / 最大活躍 initialSize5 minIdle5 maxActive20 # 獲取連接的超時時間單位毫秒 maxWait60000參數說明characterEncodingutf8mb4解決的是中文亂碼問題utf8mb4比utf8多覆蓋了 emoji 和特殊生僻字醫(yī)藥名里偶爾會出現這類字符。maxWait60000的意思是請求連接最多等 60 秒超過就拋異常而不是無限阻塞——演示現場最尷尬的畫面就是程序卡死在“獲取連接”這一步Java 線程池里一囤任務看起來像死機。maxActive20按課設體量綽綽有余別調成 100連接數過大反而拖垮 MySQL。有個常見的玄學現象本地用 Navicat 跑 SQL 正常Java 程序查詢中文全變問號。十有八九是 Navicat 導出腳本時的編碼和 JDBC URL 不一致導出時選 UTF-8連接串也寫 UTF-8兩邊對齊就不亂碼。4. 視圖、報表與權限讓課設看起來像一個系統(tǒng)只做單表增刪改查答辯大概率被批“像大作業(yè)”。真正讓醫(yī)藥管理系統(tǒng)“系統(tǒng)感”出來的是三個東西能主動發(fā)現問題的視圖、能出報表的聚合查詢、以及能說清楚的權限模型。這一章給出這三塊的落地寫法。4.1 快過期藥品提醒把“再過 90 天失效”變成一條查詢醫(yī)藥系統(tǒng)區(qū)別于一般進銷存的核心場景是效期管理。一張藥品庫存表里有批號和效期但人工去盯太原始。用一個視圖把“臨期藥品”算出來Java 端直接SELECT * FROM v_expiring_drugs既簡單又能在答辯時講“用視圖屏蔽了復雜條件”。CREATE VIEW v_expiring_drugs AS SELECT d.drug_code, d.drug_name, s.batch_no, s.expire_date, s.quantity, DATEDIFF(s.expire_date, CURDATE()) AS remain_days FROM drug_stock s JOIN drug_info d ON s.drug_id d.id WHERE s.expire_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 90 DAY) AND s.quantity 0 ORDER BY s.expire_date ASC;DATE_ADD(CURDATE(), INTERVAL 90 DAY)是 MySQL 的日期函數90 天這個閾值你可以改成180或30改成參數后更適合做“預警策略”。視圖的好處是條件只維護在一處Java 端的查詢代碼不會到處散落expire_date判斷。這個視圖在 ER 圖里不需要畫出來但報告里寫一段“基于視圖的臨期預警模塊”會讓評委老師覺得你對數據庫的掌握超出“會建表”一個層級。4.2 月度銷售統(tǒng)計GROUP BY 與日期索引的一個典型配合報表是數據庫課設的高頻考點。下面這條 SQL 統(tǒng)計最近 30 天每個藥品的銷售數量和銷售額SELECT d.drug_code, d.drug_name, SUM(i.quantity) AS total_qty, SUM(i.amount) AS total_amount, COUNT(DISTINCT o.id) AS order_count FROM sales_order_item i JOIN sales_order o ON o.id i.order_id JOIN drug_info d ON d.id i.drug_id WHERE o.create_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND o.sale_status 1 GROUP BY d.drug_code, d.drug_name ORDER BY total_amount DESC LIMIT 20;關鍵點有兩個第一WHERE o.create_time ...如果量大了必須配合sales_order.create_time上的索引否則每次統(tǒng)計都全表掃第二GROUP BY的列要和SELECT的非聚合列一致drug_code、drug_name否則在 MySQL 5.7 以上會直接報ONLY_FULL_GROUP_BY錯誤這是課設里被問爆的一個報錯后面避坑章會專門展開。這里還有一個面試官愛問的優(yōu)化點COUNT(DISTINCT o.id)在數據量大時開銷不小如果只是演示可以去掉這個字段或改成“訂單數 ≈ 明細條數”減少一層計算。報表類 SQL 要在“演示夠用”和“理論正確”之間取平衡真拿千萬級數據來壓LIMIT 20 只是保底手段。4.3 權限模型三張表解決“誰說能看報表”醫(yī)藥管理系統(tǒng)繞不開的角色無非是“系統(tǒng)管理員”“藥房操作員”“報表查看者”。如果你的課設只有一張登錄表和一段if (username.equals(admin))數據庫層就太單薄了。常見做法是用最樸素的 RBAC基于角色的訪問控制——三張表-- 用戶表 CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, real_name VARCHAR(20), role_id INT NOT NULL ); -- 角色表 CREATE TABLE sys_role ( id INT AUTO_INCREMENT PRIMARY KEY, role_name VARCHAR(30) NOT NULL UNIQUE, role_desc VARCHAR(100) ); -- 菜單/功能權限表user 通過 role 獲得權限 CREATE TABLE sys_menu ( id INT AUTO_INCREMENT PRIMARY KEY, menu_code VARCHAR(50) NOT NULL UNIQUE, menu_name VARCHAR(50), parent_id INT DEFAULT 0 ); -- 角色-菜單關聯表 CREATE TABLE sys_role_menu ( role_id INT NOT NULL, menu_id INT NOT NULL, PRIMARY KEY (role_id, menu_id) );三張實體表再加一張關聯表登錄之后查一次角色再查一次菜單集合就能在前端控制“操作員看不到成本價、只有管理員能點盤點”。注意password_hash字段這里只放了 64 字符占位實際課設里可以直接用 Java 的BCrypt或SHA-256但一定要在報告里寫清楚“沒有存明文密碼”——這一句話就能避開答辯時“你這密碼安全嗎”的追問。5. 避坑手冊醫(yī)藥系統(tǒng)里最容易翻車的 5 個點這一章寫給已經建完表、正準備聯調的你。以下 5 個坑都是真實出現過的每一條都用“現象→原因→解決”的方式展開對著排查能省下一個通宵。5.1 金額算錯float 浮點精度引發(fā)的“對不上賬”現象銷售訂單明細里顯示單價 9.9 元數量 3 盒總金額卻是 29.699999 元。明細單條看不出來月度匯總一跑小數位全亂。原因建表時把price、amount定義成了FLOAT。浮點數是二進制近似存儲9.9 在內存里并不是精確的 9.9乘 3 之后誤差被放大。解決金額列全部改成DECIMAL(10,2)庫存數量按實際業(yè)務用DECIMAL(12,3)或INT。已經建錯表的用ALTER TABLE sales_order_item MODIFY price DECIMAL(10,2);逐列改。這個坑的隱蔽之處在于數據量小的時候肉眼看不出問題直到報表對賬才暴露。5.2 外鍵刪除失敗刪分類時被“正在使用”卡住現象點擊刪除某個藥品分類程序報Cannot delete or update a parent row: a foreign key constraint fails或者干脆在DELETE時卡住。原因分類表drug_category被drug_info.category_id外鍵引用系統(tǒng)拒絕刪除“有子數據的父記錄”。這不是 MySQL 故障是外鍵機制在起作用但新手往往不知道看完整錯誤信息。解決三個選項任選。第一業(yè)務上禁止物理刪除用is_deleted TINYINT DEFAULT 0邏輯刪除這是醫(yī)藥系統(tǒng)最推薦的做法第二先清空該類下所有藥品再刪分類第三刪除前把子表數據轉移到其他分類。答辯時看到這個報錯別慌順著外鍵關系說一句“這是數據庫在保護引用完整性”反而加分。5.3 并發(fā)下單把庫存扣成負數現象兩個客戶端同時售出同一種藥品最終庫存變成 -1但界面沒有任何報錯。我和很多同學一樣在這地方吃過虧。原因代碼只有“先 SELECT 庫存再 UPDATE 庫存”兩個請求同時讀、后寫的覆蓋了前寫的更新。SELECT 和 UPDATE 之間根本不是原子操作。解決回到 3.1 節(jié)那條UPDATE ... WHERE quantity ?的方式。就算 UPDATE 一行數據庫會對命中行加鎖第二次請求會等待第一次提交后才執(zhí)行要么扣減成功、要么影響行數為 0 被回滾。不要在事務里先 SELECT 再 UPDATE 來“實現業(yè)務判斷”。5.4 中文亂碼Navicat 里好端端Java 一讀就是問號現象在 Navicat 查詢窗口執(zhí)行INSERT中文正常同樣的表用 Java 程序插入庫存表里的藥品名變成???。原因連接字符集不一致。常見的是 JDBC URL 沒帶characterEncoding或者 MySQL 服務端character_set_server仍是 latin1。解決三步對齊。第一步MySQL 庫、表統(tǒng)一utf8mb4第二步JDBC URL 寫法是jdbc:mysql://localhost:3306/pharma_sys?useUnicodetruecharacterEncodingutf8mb4第三步如果用的是 Druid 連接池確認connectionProperties里沒有覆蓋掉 URL 的編碼參數。這個坑 95% 靠第二步解決剩下 5% 是代碼里建 Statement 時手動指定USE NAMES utf8mb4。5.5 GROUP BY 報錯明明查詢字段都對了還是提示 1055現象跑月度統(tǒng)計報表時MySQL 報Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column錯誤碼 1055。原因MySQL 5.7 及以上默認開啟ONLY_FULL_GROUP_BYSQL 模式要求 SELECT 的非聚合列必須出現在 GROUP BY 里或者被聚合函數包住。解決先把 SQL 寫成規(guī)范形式列出所有非聚合列如GROUP BY d.drug_code, d.drug_name。因為drug_code和drug_name都在 SELECT 里就得都寫進 GROUP BY。如果只是想在課設演示時快速跑通臨時修改sql_mode去掉ONLY_FULL_GROUP_BY也行但報告里最好別拿出來說——答辯老師通常認為這是治標不治本的妥協(xié)。6. 驗收自測課堂演示前要跑通的三個場景系統(tǒng)寫完后給自己做一次“模擬驗收”。這一步的價值在于把數據庫層面的特性真正演示出來而不是靠 PPT 念一遍。下面這三個場景能覆蓋事務、并發(fā)、優(yōu)化三個高頻扣分點。第一個場景是事務回滾。開一個事務故意插入一個不存在的drug_id銷售明細存儲過程應當返回“藥品不存在”并整體回滾。驗證方法很簡單事務執(zhí)行前記錄sales_order的最大 id執(zhí)行失敗后重新查詢該 id 不存在sales_order_item中也沒有半截數據。答辯時說“要么全部成功要么全部不成功”這句話就是用START TRANSACTION和ROLLBACK頂起來的。第二個場景是并發(fā)扣庫存。準備一個庫存量為 10 的藥品開兩個客戶端同時各買 6 盒。結果應當是一個成功、一個提示庫存不足最終庫存為 4 而不是 -2。這里要向老師指出成功方用UPDATE ... WHERE quantity ?拿到行鎖失敗方等待鎖釋放后匹配不到記錄影響行數為 0 被回滾。我強烈建議在答辯前實際演示一次因為并發(fā)的不確定性會讓很多“理論上正確”的程序當場翻車。第三個場景是慢查詢分析。先在sales_order.create_time建索引再跑月度銷售統(tǒng)計 SQL執(zhí)行EXPLAIN看type和rowsEXPLAIN SELECT ... FROM sales_order_item i JOIN sales_order o ON o.id i.order_id WHERE o.create_time 2024-01-01;重點關注三列type是否為ref或rangekey是否真的用了idx_create_timerows是否遠小于全表行數。如果顯示ALL全表掃描就是索引沒生效最常見原因是 WHERE 條件里的列和索引列類型不一致或者create_time在函數里包了一層。這個EXPLAIN跑完數據庫優(yōu)化這一塊的分數基本就拿到了。最后說一個每次答辯都會被追問的細節(jié)備份與恢復。課設報告里寫清楚“用mysqldump做每日備份恢復時用SOURCE導回”再加上一句“實際生產環(huán)境不能只靠單機備份”。醫(yī)藥數據屬于強監(jiān)管數據這句話能讓你和只寫了一堆 CRUD 的同學明顯拉開距離。我在做這類系統(tǒng)的課設時養(yǎng)成的一個習慣是在每個核心表上都留一個create_time和update_time字段哪怕當時用不上。后面加審核、加審計、加數據對比都會感謝當時的這個決定。數據庫課設最怕的不是功能少而是不給自己留余地。希望這份從建表到避坑的整套做法能幫到你至少讓你在后半夜聯調時少走兩條彎路。本文還有配套的精品資源點擊獲取