
简介本资源为「合理用药信息系统设计与实现」本科毕业设计的中期答辩演示文稿面向计算机与医疗信息化方向的毕业生及答辩准备者可用于梳理课题逻辑、模拟答辩陈述或参考系统分析类PPT的结构编排。压缩包内含1个pptx文件大小约866KB以幻灯片形式呈现绪论、需求分析、系统分析、步骤与进度等核心章节。内容围绕世界卫生组织关于药物不合理使用的背景数据展开依次给出业务用例模型、边界定义、组织机构分析、药品信息采集的人员活动图与系统用例图、合理性指标分析模型及业务规则等关键建模成果并配套2017年12月至2018年5月的分阶段进度安排从文献调研、概念模型与设计模型构建到系统开发测试与论文撰写逐项推进便于读者快速把握一个完整毕业设计从选题到落地的论证脉络与图表组织方式。目前已有117人学习适合需要答辩思路与建模范例的读者参考。1. 合理用药信息系统到底解决什么门诊高峰时段一位药师平均只有几十秒扫一张处方抽查处方的比例常常不到 5%配伍禁忌、重复用药、剂量超限这些风险基本靠事后点评来补。合理用药信息系统的设计与实现要解决的核心问题就是把审核动作从「事后抽查」挪到「开方那一瞬间」让规则库在医生点击保存时完成一次全量校验并把分级告警直接推回开方界面。它不是给医院再加一个查询页面而是一条串起药品字典、相互作用规则、医师权限、处方点评的数据链路。做这个方向的毕业设计技术栈通常已经定成 Spring Boot 加 Vue 加 MySQL真正卡人的是规则怎么建模、审核怎么在不拖慢门诊的前提下跑完、答辩时怎么用数据证明它有效。信息系统这类题目最怕只画用例图和 E-R 图把审核引擎的实现和指标验证补上答辩现场才站得住。2. 药品知识库与合理用药规则的建模合理用药信息系统的底座是药品知识库规则引擎跑得快不快、误报多不多八成取决于这一层的表怎么设计。常见的翻车方式是给每一种药单独写一条 Java 判断逻辑规则到几百条就维护不动更别说答辩时解释新增一条规则的代价。把知识变成数据、把判定变成查表是这套系统能扩展的前提。下面按药品主数据、规则统一结构、数据导入三步走。2.1 药品主数据表要预留哪些字段药品字典表不是简单的名称加编码审核要用到的属性都得在建表时想清楚否则后期加字段会牵动规则引擎。下面这张表是常见做法里比较完整的一个版本。字段类型说明drug_idBIGINT主键自增drug_codeVARCHAR(32)国家药品编码与院内 HIS 对照generic_nameVARCHAR(128)通用名审核展示用trade_nameVARCHAR(128)商品名重复用药判定要一起看atc_codeVARCHAR(16)药理分类类别级规则靠它dosage_formVARCHAR(32)剂型注射、口服、外用specVARCHAR(64)规格adult_dose_minDECIMAL(10,3)成人单次剂量下限adult_dose_maxDECIMAL(10,3)成人单次剂量上限is_antibioticTINYINT是否抗菌药物antibiotic_levelTINYINT0 非抗菌、1 非限制、2 限制、3 特殊statusTINYINT1 启用、0 停用atc_code这一列值得单独说。很多相互作用规则是按药理类别定义的比如「两种 ACEI 联用」属于重复用药「两种 QT 间期延长药物联用」属于需要警示的相互作用。如果逐对药品配置一个类别里十种药就要配 45 对用 ATC 前 5 位做前缀匹配一条规则就覆盖整个类别规则量能压下一个数量级。做设计与实现时把这个取舍讲清楚比堆十几个功能点更有说服力。2.2 相互作用、配伍禁忌、重复用药的统一表结构三类规则看起来差别很大落到数据结构上高度相似都是「一组药品或药品类别在某种条件下产生某个级别的提醒」。所以用一张规则表承载用rule_type区分类型用severity区分级别是更省事的做法。字段类型说明rule_idBIGINT主键rule_typeTINYINT1 相互作用、2 配伍禁忌、3 重复用药、4 剂量超限、5 特殊人群severityTINYINT1 禁忌、2 严重、3 一般、4 提示drug_a_idBIGINT药品 Adrug_b_idBIGINT药品 B单药规则填 0atc_a_prefixVARCHAR(8)类别级规则的 A 前缀可空atc_b_prefixVARCHAR(8)类别级规则的 B 前缀可空condition_jsonJSON年龄、孕周、肾功能等触发条件adviceVARCHAR(500)给医生的处置建议sourceVARCHAR(128)规则出处答辩时会被追问statusTINYINT1 启用、0 停用condition_json是为了让同一条规则能挂条件而不用建新表。比如「肾功能不全患者使用某药需减量」可以写成{renalClearanceMax: 30, ageMin: 65, adviceLevel: reduce}建表语句如下注意药品对的联合索引审核时是热点查询路径。CREATE TABLE drug_rule ( rule_id BIGINT NOT NULL AUTO_INCREMENT, rule_type TINYINT NOT NULL COMMENT 1相互作用 2配伍禁忌 3重复用药 4剂量 5特殊人群, severity TINYINT NOT NULL COMMENT 1禁忌 2严重 3一般 4提示, drug_a_id BIGINT NOT NULL, drug_b_id BIGINT NOT NULL DEFAULT 0, atc_a_prefix VARCHAR(8) DEFAULT NULL, atc_b_prefix VARCHAR(8) DEFAULT NULL, condition_json JSON DEFAULT NULL COMMENT 触发条件为空表示无条件触发, advice VARCHAR(500) NOT NULL COMMENT 医生可见建议, source VARCHAR(128) DEFAULT NULL COMMENT 说明书/药典/文献出处, status TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (rule_id), KEY idx_drug_pair (drug_a_id, drug_b_id), KEY idx_type_status (rule_type, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT合理用药规则表;drug_b_id默认 0 而不是 NULL是为了让单药规则也能走同一个索引避免引擎里出现两套查询分支。2.3 规则数据的初始化与版本管理规则表建好之后导入是绕不过去的工程量。说明书里的相互作用描述是自然语言要转成药品 ID 对或 ATC 前缀对。常见做法是先从国家药品编码表把通用名和 ATC 拉进主数据再用 Excel 人工整理一份「规则种子表」导入最后用 SQL 校验有没有指向已停用药品的孤儿规则。-- 找出引用了停用药品或类别的规则导入后必查 SELECT r.rule_id, r.advice FROM drug_rule r LEFT JOIN drug d1 ON d1.drug_id r.drug_a_id AND d1.status 1 LEFT JOIN drug d2 ON d2.drug_id r.drug_b_id AND d2.status 1 WHERE r.status 1 AND (d1.drug_id IS NULL OR (r.drug_b_id 0 AND d2.drug_id IS NULL));规则还要有版本概念。答辩时评委常问「规则更新了怎么不下线系统」答案是不重启靠规则表加一个version字段和缓存刷新接口导入新版本后调一次刷新引擎重新加载。这个点写进 PPT 的难点部分很实用。参数上severity的取值必须在文档里固定下来1 到 4 分别对应拦截、强提醒、弱提醒、仅记录级别一旦被前端写死成弹窗和拦截两种后面想加中间档就要改前端。3. 处方审核引擎的实现规则进了数据库接下来是把它变成开方瞬间的一次判定。这一步的难点不在算法复杂而在响应时间一张处方平均 3 到 5 个药品明细规则表可能上万条如果每条明细都去数据库全表匹配P99 延迟会直接顶到门诊系统超时的边界。常见做法是启动时把规则全部加载进内存用哈希索引把「药品对」压成 O(1) 查询审核过程不碰数据库只在最后落一条日志。3.1 审核入参标准化处方 DTO 与患者上下文引擎的输入不只是药品列表还必须有患者上下文否则带条件的规则根本没法判。把入参先标准化成一个 DTO是引擎能做单元测试的前提。public class PrescriptionCheckRequest { private Long patientId; private Integer age; // 岁 private BigDecimal weight; // kg private Boolean pregnant; // 是否妊娠 private BigDecimal renalClearance;// 肌酐清除率 mL/min private BigDecimal liverAlt; // ALT U/L private ListItem items; public static class Item { private Long drugId; private String atcCode; private BigDecimal singleDose; // 单次剂量 private BigDecimal frequency; // 每日次数 private String route; // 给药途径 // getter / setter 省略 } }缺上下文的处理要提前定好renalClearance为空时不触发肾功能相关规则而不是按 0 处理。很多实现误报高就是因为把空值当异常值结果所有老年患者都被刷出一堆减量提醒。判定时用「条件字段为空则跳过该条件」的语义更稳妥。3.2 基于内存索引的规则匹配加载阶段把规则分成两类药品对规则进pairIndex单药规则进singleIndex。药品对规则无论医嘱顺序如何都要命中所以索引键取两个 ID 排序后拼接。Component public class DrugRuleEngine { private final MapString, ListDrugRule pairIndex new ConcurrentHashMap(4096); private final MapLong, ListDrugRule singleIndex new ConcurrentHashMap(2048); private final MapString, ListDrugRule atcPairIndex new ConcurrentHashMap(2048); PostConstruct public void warmUp() { for (DrugRule rule : ruleMapper.selectEnabled()) { if (rule.getAtcAPrefix() ! null rule.getAtcBPrefix() ! null) { atcPairIndex.computeIfAbsent( rule.getAtcAPrefix() _ rule.getAtcBPrefix(), k - new ArrayList()).add(rule); } else if (rule.getDrugBId() ! null rule.getDrugBId() 0) { pairIndex.computeIfAbsent(pairKey(rule.getDrugAId(), rule.getDrugBId()), k - new ArrayList()).add(rule); } else { singleIndex.computeIfAbsent(rule.getDrugAId(), k - new ArrayList()).add(rule); } } } /** 药品 ID 排序后拼键保证 (a,b) 与 (b,a) 命中同一条规则 */ private String pairKey(Long a, Long b) { return a b ? a _ b : b _ a; } }匹配时对处方明细做双重循环逐对拼键查pairIndex再用 ATC 前 5 位拼键查atcPairIndex最后每条明细查一次singleIndex。命中后统一走条件求值只有condition_json为空或全部条件满足才产出告警。这个结构的好处是可测给一个 request断言返回几条什么级别的告警即可不需要起数据库。预热的代价是启动慢几百毫秒换来的是审核全程无 SQL。规则量到十万条级别时内存占用需要留意DrugRule对象只保留判定必需字段、advice单独懒加载是很实用的优化点。3.3 严重级别分级与前端拦截策略引擎返回的告警级别最终要变成前端的动作这个映射关系必须写死成一张表避免前后端各理解一套。severity含义前端动作是否留痕1禁忌阻断保存必须修改处方强制记录2严重弹窗确认填写理由后可继续强制记录3一般侧边提示不打断流程记录4提示仅在明细行标记颜色可选级别 1 的规则要谨慎配置。把所有相互作用都设成禁忌医生一天要被迫改几十张处方系统上线一周就会被投诉到停用。合理做法是只把说明书明确写「禁止合用」的设成级别 1其余按临床风险降到 2 或 3这条经验在答辩里讲出来比讲技术框架更容易得分。4. 抗菌药物分级管理与处方点评的落地合理用药信息系统里抗菌药物管理往往是政策要求最硬的模块也是答辩最好出数据的地方。它包含两块开方时的权限校验和事后的处方点评统计。前者是实时拦截后者是批量计算共用同一份药品主数据和处方明细。4.1 抗菌药物三级授权的表结构抗菌药物按非限制使用、限制使用、特殊使用三级管理医师有对应授权等级。授权表要支持有效期因为医师的处方权是动态调整的。字段类型说明idBIGINT主键doctor_idBIGINT医师 IDdept_idBIGINT科室 IDantibiotic_levelTINYINT可开具的最高级别1/2/3valid_fromDATE生效日期valid_toDATE失效日期statusTINYINT1 有效、0 停用判定逻辑很直接取处方明细中is_antibiotic 1的药品用它的antibiotic_level和医师当前有效授权的antibiotic_level比较超限则走「需上级医师会诊」流程。public boolean checkAntibioticAuthority(Long doctorId, ListCheckRequest.Item items) { int allowed authorityMapper.selectMaxLevel(doctorId, LocalDate.now()); for (CheckRequest.Item item : items) { Drug drug drugCache.get(item.getDrugId()); if (drug.getIsAntibiotic() 1 drug.getAntibioticLevel() allowed) { // 越权抛出业务异常由全局处理器转成前端提示 throw new BizException(越权开具抗菌药物 drug.getGenericName()); } } return true; }selectMaxLevel要带上valid_from now AND valid_to now条件否则医师授权过期后仍能开高等级抗菌药物这是上线后审计常被点名的问题。4.2 处方点评指标的 SQL 计算处方点评是对一段时间内的处方做批量统计输出抗菌药物使用率、注射剂使用率、平均每张处方用药品种数等指标。这些指标用一条 SQL 就能算出来不必拉回 Java 循环。-- 统计某时间段内门诊处方的抗菌药物使用率与注射剂使用率 SELECT COUNT(DISTINCT p.prescription_id) AS total_rx, COUNT(DISTINCT CASE WHEN d.is_antibiotic 1 THEN p.prescription_id END) AS abx_rx, COUNT(DISTINCT CASE WHEN d.dosage_form LIKE %注射% THEN p.prescription_id END) AS inj_rx, ROUND(COUNT(DISTINCT CASE WHEN d.is_antibiotic 1 THEN p.prescription_id END) * 100.0 / COUNT(DISTINCT p.prescription_id), 2) AS abx_rate, ROUND(COUNT(DISTINCT CASE WHEN d.dosage_form LIKE %注射% THEN p.prescription_id END) * 100.0 / COUNT(DISTINCT p.prescription_id), 2) AS inj_rate FROM prescription p JOIN prescription_item i ON i.prescription_id p.prescription_id JOIN drug d ON d.drug_id i.drug_id WHERE p.create_time ? AND p.create_time ?;COUNT(DISTINCT ... CASE WHEN ...)是这类「分子分母都是处方张数」指标的通用写法关键是每张处方只算一次不能按明细行计数否则多药处方会被重复计入。时间段参数建议按自然月传入和医院上报口径对齐。点评结果落一张review_result表附带科室、医师维度前端做同比环比图就有数据源了。4.3 审核日志与误报申诉闭环审核引擎每次产出的告警都要落日志字段包括处方号、规则 ID、级别、医师的处置动作接受修改、填写理由继续、忽略。这张表的价值有两层一是答辩可以拿它算规则命中率二是医生申诉误报时有据可查。字段说明log_id主键prescription_id处方号rule_id命中的规则severity告警级别action1 修改处方、2 填理由继续、3 忽略doctor_reason继续开方时填写的原因create_time触发时间申诉闭环的做法是医生对某条告警点「误报」进入待审列表药师复核后把该规则的status置为 0 或调整condition_json。这一圈走通规则库才会越用越准而不是上线即僵化。5. 答辩前必须验证的三个指标设计和实现讲完答辩现场最容易被追问的是「你怎么知道它有用」。与其讲一堆功能不如拿出三个能量化的指标规则命中率、审核耗时、误报率。它们都能从审核日志和压测数据里直接算出来准备成本不高说服力最强。5.1 审核耗时基线怎么压用并发脚本模拟门诊高峰重点看 P99 而不是平均值。下面是一个用 JMeter 之外更轻量的方式直接起线程池打引擎接口适合本地验证。Test public void p99Latency() throws Exception { int threads 50, rounds 200; ExecutorService pool Executors.newFixedThreadPool(threads); ListLong costs Collections.synchronizedList(new ArrayList()); CountDownLatch latch new CountDownLatch(threads); for (int t 0; t threads; t) { pool.submit(() - { for (int i 0; i rounds; i) { long start System.nanoTime(); engine.check(mockRequest()); // 5 条明细的典型处方 costs.add(System.nanoTime() - start); } latch.countDown(); }); } latch.await(); Collections.sort(costs); // 取 99 分位单位折算成毫秒 System.out.println(P99 costs.get((int) (costs.size() * 0.99)) / 1_000_000.0 ms); }内存索引方案的 P99 通常在 10 到 50 毫秒之间具体取决于规则条数和明细数。把这个数字和改造前的「逐条查库」版本做对比一张 PPT 上的前后对照图比十页架构图都管用。需要注意的是压测时mockRequest要覆盖命中规则的场景全是无命中规则的请求测出来偏乐观。5.2 误报抑制的三条经验规则误报是合理用药系统落地的头号杀手也是答辩时最能体现思考深度的地方。第一条条件规则缺患者上下文时一律降级为提示不按异常值处理避免老年患者被批量刷减量提醒。第二条同一药品对命中多条规则时只展示最高级别的一条合并建议文本否则一张处方弹七八个窗口医生会直接关掉。第三条外用制剂和局部用药不参与全身相互作用判定比如外用激素和外用抗真菌药按给药途径route过滤掉能砍掉相当比例的无意义命中。误报率可以用日志表里action 3且经药师复核确认误报的条数除以总告警条数来算。答辩前跑一个月的真实或模拟数据如果误报率能从初版的 20% 压到 5% 以内这就不是一句「系统已实现」而是有证据的工程结论。最后补一个可操作的小技巧把规则按source分组统计命中率长期零命中的规则要么是条件写错了要么是适应症范围内本就不会触发答辩前逐条筛一遍能同时提高命中率数字和规则库的可信度。本文还有配套的精品资源点击获取