ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医院管理系统课程设计:从ER建模到Flask后端的完整实现

医院管理系统课程设计:从ER建模到Flask后端的完整实现

简介:本资源是一份面向计算机专业本科生的C语言课程设计实践项目,聚焦医院管理系统的完整实现与总结报告,适用于软件工程、数据库原理及C语言综合实训等教学场景。项目采用纯C语言开发,集成SQL数据库操作(如SQLite/MySQL C API)与基础GUI界面,覆盖患者管理、预约挂号、诊疗记录等核心业务逻辑,帮助学习者贯通编程、数据结构、API调用与关系型数据库应用能力。压缩包共16个文件,含3个头文件(.h)定义数据结构与接口、2个C++源码(.cpp)实现主逻辑与对话框、2个目标文件(.o)及1个可执行程序(.exe),辅以资源脚本(.rc/.res/.ico)、文档(.docx)和工程配置文件(.cfp),整体体积566KB,结构清晰,便于编译调试与代码研读。已有1096人学习下载,提供从需求分析、系统设计、C语言编码、SQL建表与查询到完整总结报告的全流程交付物,特别适合课程设计参考、期末项目复盘及C+数据库协同开发入门实践。

1. 医院管理系统的课程设计(含总结报告):一个能跑通、能答辩、能写进简历的完整闭环方案

这不是一份“画大饼”的系统架构图,也不是只贴几张UI截图就交差的PPT作业。它是一套从需求建模到数据库落地、从前端交互到后端逻辑、从测试用例到总结报告全文可复现的课程设计实战路径。某高校计算机专业连续三年将该选题列为《软件工程》《数据库原理》《Web开发基础》三门课的联合大作业,核心诉求很实在:学生得在3~4周内,独立完成一个有真实业务约束、有数据流转逻辑、有可演示界面、有文档支撑的轻量级医院管理原型。它不追求替代HIS系统,但必须体现挂号、分诊、医生排班、病历录入、药品库存五个关键子域的关联性——比如“患者挂号后,分诊台能查到候诊队列;医生接诊时能调取该患者历史用药记录;开出处方后,药房库存自动扣减”。我带过七届学生做这个课题,最常翻车的不是代码写不出来,而是需求没对齐、ER图和表结构脱节、报告里写不出“为什么选MySQL而不是SQLite”这种细节。本文就带你把这整条链路一节一节拧紧,每一步都附可粘贴运行的SQL、可调试的Python/Java片段、可直接套用的报告段落模板。


2. 用实体关系建模锁定业务边界:从挂号单、处方笺到药房库存的5个核心实体怎么画才不返工

课程设计最容易被导师当场叫停的环节,就是ER图答辩。很多同学画出的图里,“患者”和“医生”之间连着一条粗线,旁边标注“就诊”,但没人能说清这条线背后是1对1、1对多还是多对多,更答不出“如果患者复诊,历史病历如何关联新挂号记录”。我们必须先锚定五个不可拆解的核心实体及其约束条件——这是后续所有开发的宪法。

2.1 患者与挂号:为什么“挂号记录”必须是独立实体而非“患者”属性?

直觉上,“挂号时间”“科室”“医生”似乎可以作为患者表的字段。但业务规则强制要求:

  • 同一患者可多次挂号(复诊、跨科就诊);
  • 每次挂号生成唯一号源(如G20240521001),需独立追踪状态(已签到/已就诊/已取消);
  • 挂号费、医保类型、预约渠道(窗口/APP/电话)等属性仅属于本次挂号行为。

因此,“挂号记录”必须是独立实体,且与“患者”构成一对多关系(一个患者对应多条挂号记录)。其主键不能是患者ID,而应是自增ID或组合键(挂号日期+序号)。常见错误是把挂号ID设为外键指向患者表主键,导致无法删除患者时级联误删历史挂号——实际应设为非级联删除,保留医疗行为审计线索。

-- 正确建模:挂号记录表独立存在,患者ID仅为外键 CREATE TABLE registration ( reg_id CHAR(12) PRIMARY KEY, -- 格式:GYYYYMMDDNNN,如G20240521001 patient_id INT NOT NULL, -- 外键,指向patient表 dept_id INT NOT NULL, -- 科室ID,非空确保挂号必选科室 doctor_id INT, -- 预约医生ID,可为空(未指定医生时由分诊分配) reg_time DATETIME DEFAULT NOW(), -- 挂号时间,精确到秒 status ENUM('pending','checked_in','consulted','cancelled') DEFAULT 'pending', fee DECIMAL(6,2) NOT NULL, -- 挂号费,非空体现收费动作 channel ENUM('window','app','phone') DEFAULT 'window', FOREIGN KEY (patient_id) REFERENCES patient(patient_id) ON DELETE RESTRICT, FOREIGN KEY (dept_id) REFERENCES department(dept_id) ON DELETE RESTRICT, FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ON DELETE SET NULL );

提示:ON DELETE RESTRICT是关键。当某患者信息需修正(如身份证号录入错误),管理员应更新patient表,而非删除重建——否则所有历史挂号记录将丢失。ON DELETE SET NULL用于doctor_id,因医生离职时,其历史挂号记录仍需保留,仅将医生字段置空即可。

2.2 处方与药品库存:如何用“处方明细”桥接开方行为与库存扣减?

处方不是一张静态纸,而是动态触发库存变更的动作。学生常犯的错是:

  • 在prescription表中直接存药品名称和数量(字符串拼接),导致无法关联药品库、无法校验库存、无法统计药品消耗;
  • 忽略“处方审核”环节,开方即扣库存,违反医疗安全规范。

正确做法是引入处方明细(prescription_detail)作为桥接表,它同时关联处方主表和药品主表,并承载单次开药的数量、单位、用法。库存扣减必须发生在“处方审核通过”事件之后,且需事务保证:审核成功则库存原子性减少,失败则回滚。

-- 处方主表:仅存审核状态、开方医生、审核医生、审核时间 CREATE TABLE prescription ( presc_id INT PRIMARY KEY AUTO_INCREMENT, reg_id CHAR(12) NOT NULL, -- 关联挂号记录 doctor_id INT NOT NULL, -- 开方医生 checker_id INT, -- 审核医生,可为空(待审核状态) check_time DATETIME, -- 审核时间,为空表示未审核 status ENUM('draft','checked','rejected') DEFAULT 'draft', created_at DATETIME DEFAULT NOW(), FOREIGN KEY (reg_id) REFERENCES registration(reg_id) ON DELETE CASCADE, FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ON DELETE RESTRICT, FOREIGN KEY (checker_id) REFERENCES doctor(doctor_id) ON DELETE SET NULL ); -- 处方明细表:精准绑定药品与用量,支持多药同方 CREATE TABLE prescription_detail ( detail_id INT PRIMARY KEY AUTO_INCREMENT, presc_id INT NOT NULL, drug_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), -- 单次开药数量,必须大于0 unit VARCHAR(10) NOT NULL, -- 单位:盒、瓶、支、片 usage TEXT, -- 用法:口服、外用、每日三次等 FOREIGN KEY (presc_id) REFERENCES prescription(presc_id) ON DELETE CASCADE, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ON DELETE RESTRICT ); -- 药品库存表:实时反映可用数量,含预警阈值 CREATE TABLE drug_stock ( stock_id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, available_quantity INT NOT NULL DEFAULT 0, min_threshold INT NOT NULL DEFAULT 10, -- 低于此值触发库存预警 updated_at DATETIME DEFAULT NOW() ON UPDATE NOW(), FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ON DELETE CASCADE );

参数说明:available_quantity必须设为NOT NULL DEFAULT 0,避免NULL值导致库存计算逻辑崩溃;min_threshold不写死数字,而是作为可配置字段,方便后期扩展预警规则(如贵重药品阈值设为5,普通药品设为20)。

2.3 分诊台与候诊队列:用状态机模型替代“正在叫号”这种模糊字段

分诊逻辑常被简化为“当前叫号:张三”,但真实场景需支持:

  • 按科室/医生分流候诊队列;
  • 患者可签到(从“待签到”变“已签到”),系统自动排序;
  • 医生可临时暂停叫号(设备故障),队列状态需持久化;
  • 过号患者可重排(非删除后重挂)。

因此,“分诊状态”不应是字符串字段,而应是状态机驱动的独立表,每个挂号记录的状态变迁需留痕。

-- 分诊状态表:定义合法状态及转换规则 CREATE TABLE triage_status ( status_id TINYINT PRIMARY KEY, status_name VARCHAR(20) NOT NULL UNIQUE, description TEXT ); INSERT INTO triage_status VALUES (1, 'pending', '已挂号未签到'), (2, 'checked_in', '已签到,等待叫号'), (3, 'called', '已叫号,前往诊室'), (4, 'consulting', '正在就诊'), (5, 'completed', '就诊完成'), (6, 'overdue', '过号,需重排'); -- 分诊记录表:每次状态变更生成新记录,形成审计链 CREATE TABLE triage_log ( log_id BIGINT PRIMARY KEY AUTO_INCREMENT, reg_id CHAR(12) NOT NULL, from_status TINYINT, to_status TINYINT NOT NULL, operator VARCHAR(20) NOT NULL, -- 操作人:分诊员工号或系统标识 operated_at DATETIME DEFAULT NOW(), note TEXT, -- 备注:如“患者迟到15分钟,重排至队尾” FOREIGN KEY (reg_id) REFERENCES registration(reg_id) ON DELETE CASCADE, FOREIGN KEY (to_status) REFERENCES triage_status(status_id), FOREIGN KEY (from_status) REFERENCES triage_status(status_id) );

逻辑说明:不直接在registration表加status字段,是因为状态变更需审计。例如患者签到时,插入一条to_status=2的日志;医生点击“叫号”时,再插一条to_status=3的日志。这样即使系统崩溃,也能通过最新日志还原当前状态,而非依赖单字段值——后者在并发操作下极易出错。


3. 用Python Flask+MySQL实现最小可行后端:挂号、分诊、开方三个接口的事务安全写法

前端页面可以简陋(Bootstrap表格+按钮),但后端逻辑必须经得起推敲。课程设计答辩时,导师最爱问:“如果两个分诊员同时给同一个患者点击‘叫号’,会不会重复叫?”、“处方审核时库存不足,是回滚整个事务还是只提示用户?”。本节用Flask实现三个高危接口,重点展示事务控制、并发防护、错误回滚的真实写法,拒绝try...except pass式玄学容错。

3.1 挂号接口:防重复提交与号源唯一性双重校验

挂号看似简单,但存在两个致命并发点:

  • 用户快速双击“确认挂号”按钮,前端未禁用,导致同一表单提交两次;
  • 多个窗口/APP同时挂号,生成相同号源(如G20240521001)。

解决方案:前端按钮置灰 + 后端号源生成时加数据库唯一约束 + 事务内重试机制。

# app.py from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy import datetime import random app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:password@localhost/hospital' db = SQLAlchemy(app) class Registration(db.Model): __tablename__ = 'registration' reg_id = db.Column(db.String(12), primary_key=True) patient_id = db.Column(db.Integer, nullable=False) dept_id = db.Column(db.Integer, nullable=False) # ... 其他字段 @app.route('/api/register', methods=['POST']) def create_registration(): data = request.get_json() patient_id = data.get('patient_id') dept_id = data.get('dept_id') # 1. 生成号源:日期+3位随机数(非自增,避免暴露挂号量) today = datetime.date.today().strftime('%Y%m%d') for _ in range(10): # 最多重试10次,避免死循环 suffix = f"{random.randint(100, 999)}" reg_id = f"G{today}{suffix}" try: # 2. 在事务中插入,利用唯一索引捕获重复 with db.session.begin_nested(): # 使用嵌套事务,便于回滚 reg = Registration( reg_id=reg_id, patient_id=patient_id, dept_id=dept_id, fee=15.00, channel='app' ) db.session.add(reg) db.session.flush() # 触发INSERT,但不提交 break # 成功则跳出重试 except Exception as e: if "Duplicate entry" in str(e): continue # 号源重复,重新生成 else: raise e # 其他错误抛出 db.session.commit() return jsonify({"success": True, "reg_id": reg_id})

参数说明:db.session.begin_nested()创建保存点,db.session.flush()强制执行INSERT但不提交,此时若违反唯一约束会立即报错,continue触发下一轮号源生成。最终db.session.commit()提交整个事务。比在应用层查重再插入更可靠——查重与插入之间仍有时间窗口。

3.2 分诊叫号接口:乐观锁防止并发覆盖

叫号操作本质是更新挂号记录的状态。若无防护,A分诊员读取到患者状态为“已签到”,B分诊员也读取到同样状态,两人同时点击“叫号”,都会将状态改为“已叫号”,造成逻辑错误(患者被叫两次)。传统悲观锁(SELECT ... FOR UPDATE)在Web应用中易引发连接阻塞。我们采用乐观锁 + 版本号字段:

# 在registration表中增加version字段 # ALTER TABLE registration ADD COLUMN version INT DEFAULT 1; class Registration(db.Model): # ... 其他字段 version = db.Column(db.Integer, default=1) @app.route('/api/triage/call/<reg_id>', methods=['POST']) def call_patient(reg_id): try: # 1. 带版本号条件更新:仅当当前version=读取值时才更新 result = db.session.execute( text("UPDATE registration SET status='called', version=version+1 WHERE reg_id=:rid AND version=:ver"), {"rid": reg_id, "ver": 1} # 实际应先SELECT获取当前version ) if result.rowcount == 0: # 更新失败:说明version已变,其他人在操作 return jsonify({"error": "患者状态已被他人修改,请刷新后重试"}), 409 db.session.commit() return jsonify({"success": True}) except Exception as e: db.session.rollback() return jsonify({"error": "系统繁忙,请稍后重试"}), 500

血泪经验:初版代码直接SELECT * FROM registration WHERE reg_id=?,然后UPDATE ... WHERE reg_id=? AND version=?,但SELECT与UPDATE之间仍有并发窗口。正确做法是在UPDATE语句中直接嵌入子查询获取当前version,或使用数据库原生的UPDATE ... SET version=version+1 WHERE ... AND version=(SELECT version FROM registration WHERE reg_id=?)。此处为简化演示,实际项目中应封装为原子操作。

3.3 处方审核接口:库存扣减的事务边界与补偿机制

审核处方是典型分布式事务场景:需同时更新处方状态、扣减库存、记录日志。任一环节失败,必须全部回滚。但MySQL不支持跨库事务,若未来药房系统独立部署,需预留补偿接口。当前单库方案,用with db.session.begin():显式声明事务边界。

@app.route('/api/prescription/check/<presc_id>', methods=['POST']) def check_prescription(presc_id): try: with db.session.begin(): # 整个块为一个事务 # 1. 查询处方明细,获取所有药品ID和数量 details = db.session.execute( text("SELECT drug_id, quantity FROM prescription_detail WHERE presc_id=:pid"), {"pid": presc_id} ).fetchall() # 2. 检查每种药品库存是否充足 for drug_id, qty in details: stock = db.session.execute( text("SELECT available_quantity FROM drug_stock WHERE drug_id=:did FOR UPDATE"), {"did": drug_id} ).scalar() if stock is None or stock < qty: raise ValueError(f"药品ID {drug_id} 库存不足,当前{stock},需{qty}") # 3. 扣减库存(FOR UPDATE已加锁,避免并发扣减) for drug_id, qty in details: db.session.execute( text("UPDATE drug_stock SET available_quantity = available_quantity - :qty WHERE drug_id = :did"), {"qty": qty, "did": drug_id} ) # 4. 更新处方状态为'checked' db.session.execute( text("UPDATE prescription SET status='checked', checker_id=:uid, check_time=NOW() WHERE presc_id=:pid"), {"uid": 1001, "pid": presc_id} # 实际应从session获取当前登录医生ID ) # 5. 记录审核日志(可选) db.session.execute( text("INSERT INTO audit_log (action, target_id, operator) VALUES ('prescription_check', :pid, :uid)"), {"pid": presc_id, "uid": 1001} ) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: db.session.rollback() # 事务自动回滚 return jsonify({"error": "审核失败,请重试"}), 500 return jsonify({"success": True, "message": "处方审核通过,库存已扣减"})

关键点:FOR UPDATE在库存查询时加行锁,确保扣减前库存值未被其他事务修改;with db.session.begin():确保所有操作在同一个事务中,异常时自动回滚。不要用db.session.commit()分散提交——那是事务失控的开端。


4. 前端交互与数据可视化:用ECharts画出科室挂号热度图,让答辩PPT多一个亮点

课程设计答辩时,纯文字描述“系统实现了分诊功能”远不如一张动态图表有说服力。某实验室曾要求学生在总结报告中加入“近7天各科室挂号量趋势图”,结果80%的学生用Excel截图糊弄。其实用ECharts+Flask API,20行代码就能生成可交互图表。本节教你如何把数据库里的原始数据,变成答辩时能让导师点头的可视化成果。

4.1 后端提供挂号量聚合API:按科室、按日期分组统计

前端图表需要结构化JSON数据,而非HTML表格。API设计必须符合ECharts的series.data格式(数组,每项为[x轴值, y轴值])。注意:日期字段需转为字符串,避免JavaScript解析错误。

@app.route('/api/stats/dept_volume') def dept_volume_stats(): # 查询近7天各科室挂号量 seven_days_ago = (datetime.date.today() - datetime.timedelta(days=6)).strftime('%Y-%m-%d') # 使用原生SQL避免ORM复杂关联,提升性能 sql = """ SELECT d.dept_name AS name, DATE(r.reg_time) AS date, COUNT(*) AS count FROM registration r JOIN department d ON r.dept_id = d.dept_id WHERE r.reg_time >= :start_date GROUP BY d.dept_name, DATE(r.reg_time) ORDER BY d.dept_name, date """ rows = db.session.execute(text(sql), {"start_date": seven_days_ago}).fetchall() # 转换为ECharts所需格式:{dept1: [[date1,count1], [date2,count2]], dept2: [...]} data_dict = {} for row in rows: dept_name = row.name date_str = row.date.strftime('%m/%d') # 格式化为"05/21" count = int(row.count) if dept_name not in data_dict: data_dict[dept_name] = [] data_dict[dept_name].append([date_str, count]) return jsonify(data_dict)

参数说明:DATE(r.reg_time)提取日期部分,忽略时间;strftime('%m/%d')生成短日期标签,避免X轴拥挤;COUNT(*)统计挂号次数,非患者数(同一患者多科挂号应分别计数)。

4.2 前端ECharts初始化:5行代码渲染科室热度图

无需下载ECharts包,CDN引入即可。重点在于option配置——课程设计不需炫技,清晰传达数据即可。柱状图比折线图更适合科室对比,颜色用蓝绿系符合医疗主题。

<!-- index.html --> <div id="deptChart" style="width: 800px; height: 400px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script> const chartDom = document.getElementById('deptChart'); const myChart = echarts.init(chartDom); // 1. 初始化option(静态配置) const option = { title: { text: '近7天各科室挂号量统计' }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: [] }, // 动态填充 xAxis: { type: 'category', data: [] }, // X轴日期,动态填充 yAxis: { type: 'value' }, series: [], // 图表序列,动态填充 grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true } }; // 2. 调用API获取数据并渲染 fetch('/api/stats/dept_volume') .then(res => res.json()) .then(data => { // 提取所有日期(X轴) const allDates = new Set(); Object.values(data).forEach(series => { series.forEach(item => allDates.add(item[0])); }); const dates = Array.from(allDates).sort(); // 排序确保日期顺序 // 构建series数组 const series = []; Object.entries(data).forEach(([deptName, deptData]) => { // 将deptData转换为按dates顺序排列的count数组 const counts = dates.map(date => { const found = deptData.find(d => d[0] === date); return found ? found[1] : 0; }); series.push({ name: deptName, type: 'bar', data: counts, stack: 'total' // 堆叠显示,直观看出总量 }); }); // 更新option并渲染 option.xAxis.data = dates; option.legend.data = Object.keys(data); option.series = series; myChart.setOption(option); }); </script>

逻辑说明:stack: 'total'使各科室柱形堆叠,顶部即为当日总挂号量,比并列柱状图更能体现资源占用;tooltip.axisPointer.type = 'shadow'鼠标悬停时显示垂直阴影线,精准定位日期;Object.keys(data)动态生成图例,避免硬编码科室名。

4.3 报告中的图表解读话术:三句话讲清数据价值

答辩时,导师不会盯着图表看10秒。你必须用口语化语言,在30秒内点明图表意义。以下是某学生答辩实录(已脱敏):

“这张图展示了系统上线后一周的挂号分布。第一,内科和儿科是绝对主力,占总挂号量68%,印证了我们前期调研中‘常见病集中于这两科’的结论;第二,周五下午出现小高峰,可能与上班族下班后带孩子就诊有关,建议在排班模块中增加该时段医生权重;第三,所有科室周末挂号量低于平日40%以上,说明当前系统尚未打通社区预约渠道——这正是我们下一步优化的方向。”

提示:不要说“图表显示数据波动”,要说出谁在波动、为什么波动、对系统意味着什么。数据解读能力,比代码本身更能体现工程素养。


5. 总结报告撰写避坑指南:导师最反感的3类表述,以及5处必须写实的细节

总结报告不是技术文档的复述,而是向非技术背景的评审老师证明:“我理解了为什么这么做,且能反思局限”。某高校教务处抽查发现,72%的课程设计报告存在“假大空”问题——通篇“提高了效率”“增强了安全性”,却找不到一句具体数据支撑。本节直击答辩现场高频雷区,每条均来自真实翻车案例。

5.1 现象:用“极大提升”“显著增强”代替量化指标 → 原因:缺乏测试验证意识 → 解决:补上三组对比数据

错误示范:

“本系统极大提升了挂号效率,优化了分诊流程。”

翻车现场:导师追问:“极大是多少?比手工挂号快多少秒?分诊员每天少点几次鼠标?” 学生哑口无言。

正确写法(必须实测):

“通过模拟100次挂号操作测试:

  • 手工登记(纸质表+Excel录入)平均耗时 82±15 秒/人次;
  • 本系统在线挂号平均耗时 23±4 秒/人次,提速72%;
  • 分诊员处理单个患者状态变更(签到→叫号→完成)从平均点击6.2次降至2.1次,操作步骤减少66%。”

实操建议:用手机秒表实测,记录10次取平均;操作次数在系统中埋点统计(如console.log('triage_action: checked_in')),答辩时打开浏览器开发者工具展示。

5.2 现象:将技术选型写成“因为简单”“因为学过” → 原因:未建立决策依据框架 → 解决:用四维对比表锁定MySQL

错误示范:

“选用MySQL数据库,因为课程中学过,上手快。”

翻车现场:导师冷笑:“那SQLite更简单,为什么不用?”

正确写法(附对比表):

评估维度MySQLSQLite本系统选择理由
并发支持支持高并发读写,行级锁文件锁,写操作全局阻塞分诊、挂号、开方需同时操作,必须支持并发
事务完整性ACID完备,支持SAVEPOINTACID完备,但无SAVEPOINT处方审核需多步事务,SAVEPOINT便于局部回滚
运维成本需独立服务进程,配置稍复杂零配置,单文件课程设计环境可控,运维非重点
扩展性支持主从复制、分库分表无集群能力为未来对接LIS/PACS系统预留扩展接口

参数说明:表中“本系统选择理由”栏必须紧扣业务需求,而非技术参数。例如不写“MySQL性能更好”,而写“分诊员与挂号员同时操作不会卡顿”。

5.3 现象:回避系统缺陷,用“有待完善”一笔带过 → 原因:缺乏工程反思深度 → 解决:坦诚三点局限+具体改进路径

错误示范:

“系统尚存在一些不足,有待在后续版本中完善。”

翻车现场:导师追问:“哪三点不足?为什么现在不解决?”

正确写法(示例):

“当前系统存在三项明确局限,已在代码注释及README中标注:

  1. 无患者身份核验:挂号仅校验手机号格式,未对接公安身份证API。原因:课程设计周期内无法申请政务接口。改进:预留id_card_verify字段,接入第三方SDK只需替换3处函数;
  2. 处方审核无电子签名:仅记录医生ID,未采集生物特征。原因:浏览器端无法安全调用本地证书。改进:增加扫码登录环节,用企业微信/钉钉OAuth2.0获取可信身份;
  3. 药房库存预警无通知:阈值超限仅在后台显示,未短信/邮件提醒。原因:短信网关需企业资质认证。改进:集成免费邮件服务(如SendGrid沙箱),5行代码即可发送预警邮件。”

注意:每条“局限”必须包含现象、根因、短期绕过方案、长期改进路径四要素。导师想看到的是你的问题拆解能力,而非完美主义幻觉。


6. 答辩前最后一小时 checklist:3个必验场景、2个防翻车技巧、1个让导师记住你的细节

答辩不是代码展示,而是信任建立。导师在15分钟内要判断:“这学生是否真做过、是否懂原理、是否有工程潜质”。我带过的最惊艳的一次答辩,学生没讲一行代码,只做了三件事:

  1. 打开系统,现场演示“患者张三挂号→分诊员李四叫号→王医生开处方→药房赵五审核→库存自动扣减”全流程;
  2. 当导师问“如果王医生开错药,怎么撤回?”时,他立刻切到数据库,执行UPDATE prescription SET status='draft' WHERE presc_id=1001,并解释:“审核后状态不可逆,但管理员有紧急回退权限,日志全程留痕”;
  3. 最后一页PPT只有一句话:“本系统所有SQL语句均通过Explain分析,95%查询走索引,慢查询日志已归档至report/slow_query.log”。

这就是专业感。以下是你答辩前必须亲手验证的清单:

6.1 三个必验场景(每项限时3分钟)

场景验证方法通过标准
并发挂号不重复开两个浏览器窗口,同一患者ID、同一科室,连续点击“挂号”按钮10次数据库中仅生成1条挂号记录,号源不重复
库存扣减不超卖查看某药品库存为5,开一张处方含该药6片,提交审核返回“库存不足”错误,数据库库存仍为5,处方状态保持'draft'
状态变更可追溯对同一挂号记录,依次执行:签到→叫号→就诊→完成→过号(手动更新状态)triage_log表中生成5条记录,to_status字段值严格按1→2→3→4→6递进

提示:这些场景必须在答辩电脑上实操,而非“理论上可以”。我见过太多学生说“应该没问题”,结果答辩时点一下就报500错误——因为忘了启动MySQL服务,或.env文件密码写错。

6.2 两个防翻车技巧(写在便利贴上,贴屏幕边)

  • 技巧1:SQL注入防御自查
    所有接收用户输入的接口(如/api/register),检查是否使用?占位符或text()参数化查询。严禁出现f"INSERT INTO ... WHERE name='{name}'"。在答辩PPT中放一张截图:print(db.session.bind.dialect.statement_compiler(db.session.bind.dialect, stmt))输出的预编译SQL,证明参数已转义。

  • 技巧2:外键约束启用验证
    登录MySQL命令行,执行SHOW CREATE TABLE registration;,确认输出中包含FOREIGN KEY (patient_id) REFERENCES patient(patient_id)。若显示CONSTRAINT ... FOREIGN KEY ... REFERENCES ...但无ON DELETE子句,说明建表时未启用外键(InnoDB引擎需SET FOREIGN_KEY_CHECKS=1;)。

6.3 一个让导师记住你的细节:在报告附录放一份“可验证的部署清单”

别写“系统部署在本地环境”,要写:

附录A:环境可复现性声明

  • 操作系统:Windows 10 21H2 / Ubuntu 22.04 LTS
  • Python版本:3.9.18(python --version)
  • 依赖包:pip install -r requirements.txt(文件已随代码提交,共12个包,不含django等冗余框架)
  • 数据库初始化:mysql -u root -p < init_db.sql(脚本含建库、建表、基础数据INSERT,执行耗时<8秒)
  • 启动命令:cd src && python app.py(无Docker、无Nginx,纯Python启动)
  • 默认账号:admin/admin123(仅用于演示,代码中已加盐哈希)

这份清单的价值在于:它告诉导师“你考虑到了别人能否复现你的工作”。工程能力的最高体现,不是写出多炫的代码,而是让下一个接手的人,能在10分钟内跑起来。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进