去年年初,我接手了某医院的信息化系统建设,核心任务是搭一套能真正落地的电子病历管理系统。当时门诊还在用手写病历,病案室每天有人翻纸质档案,医生想找一份三年前的住院病历要花半个多小时。信息科同事说得直白:系统可以慢慢优化,但病历的录入、归档、检索和权限管控这些最底层的事必须一次做对。
这个定位很关键。病历管理系统和普通OA、电商系统完全不同,它处理的是高度隐私的患者数据,病案又是医疗机构的核心资产,接口多、角色多、数据关系复杂。最终我们采用了SpringBoot+Vue3+MyBatis的前后端分离架构,配合MySQL存储核心业务数据,把门急诊病历、住院病历、诊断、处方、医嘱、检验检查全部串了起来。本文把整套系统的设计思路、技术选型、核心表结构、后端实现、前端落地和部署阶段踩过的坑完整梳理一遍,如果你打算自己写一套医疗信息化项目,或者在公司负责相关系统研发,这篇内容应该能帮你少走不少弯路。
1. 这个系统到底在解决什么业务问题
1.1 早期病案管理的真实痛点
我接触的第一家医院还处于“半纸质半电子”状态。门诊病历是手写单据,住院病历由科室自己存Excel表格,检验科和药房各有一套独立的系统,数据互相不通。医生开完处方,患者要拿着纸质单据跑到收费窗口排队,药房再手工核对一遍药品库存。
在这种模式下,有几个问题非常要命:
- 病历检索基本靠人肉。病案室要把纸质病历扫描归档,按编号存放,调阅一份住院病历平均需要二十分钟,而且经常出现缺页、漏页、字迹无法辨认的情况。
- 数据孤岛严重。同一个患者可能多次就诊,但每次的门诊记录、住院记录、检验结果散落在不同科室,连患者本人都不一定记得全,医生自然看不到完整的既往病史。
- 权限和留痕无法保证。纸质病历谁都可以翻,护理记录、病程记录有没有被事后修改,几乎没有手段追溯。
这套系统要做的就是把这些分散的流程统一收口:患者挂一次号,从建档案、写病历、开诊断、下医嘱到检查检验结果回填,全部在一条数据链上完成。
1.2 核心角色和典型使用场景
医院系统最麻烦的不是功能多,而是角色复杂。同一个病房,护士要记录体温和护理级别,住院医生要写病程记录和查房记录,科室主任要审核病历质量和诊断合理性,医务科要抽查全院归档病历,信息科要维护账号和权限。
我们梳理了系统需要覆盖的用户角色和使用场景:
| 角色 | 主要使用场景 | 系统关注的核心能力 |
|---|---|---|
| 门诊医生 | 快速录入主诉、现病史、体格检查,开诊断和处方 | 录入效率、结构化模板、历史病历调阅 |
| 住院医生 | 书写首次病程记录、日常病程记录、出院小结 | 病历文书模板、内容分段保存、提交后锁定 |
| 护士 | 记录护理等级、体温单、医嘱执行情况 | 护理记录单、医嘱转抄 |
| 药房/检验科 | 接收并执行处方和检查申请单 | 处方明细流转、检查状态回写 |
| 科室主任/医务科 | 病历质量抽查、归档审核、统计报表 | 质控评分、多条件检索、统计导出 |
| 系统管理员 | 科室管理、角色配置、账号开通 | RBAC权限、数据字典、操作日志 |
这个角色清单直接影响后面的数据库设计。比如病历起草后需要“提交”和“归档”两个状态,归档后普通医生不能再改,只有医务科授权人员能申请修改并留下痕迹;再比如护士端需要单独的病历视图,不能直接看到处方明细以外的敏感信息。
1.3 稳定、留痕、可回溯是硬要求
医疗数据的管理原则和互联网业务差别很大。互联网产品注重体验和转化率,医疗系统则把“准确、完整、可审计”放在第一位。患者在就诊过程中的每次修改、每次查看、每次打印,都应该在系统里留下可追溯的记录。
所以这套系统除了业务表之外,还专门设计了用户操作日志表、病历修改历史表和关键数据的快照表。哪怕只是把患者的主诉内容改了一个字,也要记录操作人、操作时间、修改前后的内容摘要。理解了这个“业务优先、合规优先”的背景,再去选技术和设计数据表,思路会清晰很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:SpringBoot+Vue3+MyBatis这套组合的理由
2.1 后端框架为什么是SpringBoot
医疗类系统对技术栈的容错率很低,生产环境一旦出问题,影响的是实际诊疗流程,所以稳定压倒一切。SpringBoot能成为这类项目的常态选择,核心优势不是“新潮”,而是生态成熟、遇到问题能快速找到答案、本地开发和生产部署都非常简单。
- 内嵌Web容器,一个jar包就能启动,省去传统Tomcat独立部署的繁琐配置。
- 自动配置机制显著减少bean装配和维护成本,项目结构清晰。
- Spring生态自带的事务管理、AOP切面、拦截器机制,非常适合做操作日志、权限控制这类横切逻辑。
可能有人会问,为什么不用更新的微服务框架?答案很简单:单体架构在病历系统这个规模下完全够用。医院信息科通常只有几个人维护系统,微服务拆得越细,后续运维成本越高。先用一个工程把业务跑通,如果以后需要对接医保、互联互通、互联网医院等其他系统,再按模块拆出去。这是务实的决策,不是技术上的保守。
2.2 前端选Vue3没有争议
前后端分离已经是这类系统的标配。Vue3推出后,组合式API让组件的状态管理和逻辑复用都清爽了不少,配合TypeScript的话代码可维护性还会再上一个台阶。更重要的是Vue3周边生态已经足够成熟:官方路由、状态管理库、以及一整套可用的UI组件库,不需要自己从零造轮子。
页面上需要大量表单和表格,比如患者基本信息、诊断列表、处方明细、病历文书编辑器、统计图表。现代组件库能把这些基础交互全部覆盖,开发效率远高于传统的服务端渲染模板。我做过的项目里,一个中等复杂度的列表页,传统JSP模板至少要写三天,Vue3组件化条件下一天多就能完成,而且后期的筛选、分页、导出功能扩展非常方便。
2.3 MyBatis和MySQL的搭配逻辑
数据访问层我选了MyBatis而不是JPA,原因是医疗业务的查询条件太繁杂。同一个病历列表,门诊医生想看“我今日接诊的患者”,科室主任想看“本月未归档病历”,医务科想看“某时间段内诊断含某关键词的病历”,这些查询的过滤条件、关联表、返回字段都不一样。
MyBatis的动态SQL在这种场景下特别好用,需要什么条件就拼接什么条件,SQL优化主动权控制在开发者手里。再加上主流的PageHelper分页插件,配合MySQL的分页查询,性能表现非常稳定。
数据库选MySQL则综合考虑了成本和维护门槛。InnoDB引擎在事务、行锁、崩溃恢复方面足够满足中小型医院的业务量,百万级病历数据量配合合理索引完全能扛住。真到了三甲医院那种千万级病案的规模,再考虑引入分布式数据库或者把历史数据做冷热分层也不迟。
| 对比维度 | SpringBoot | JPA/Hibernate | MyBatis |
|---|---|---|---|
| 核心优势 | 生态成熟、自动化配置全面 | 对象映射自动、开发快 | SQL精细控制、适合复杂查询 |
| 劣势 | 相对重,但本场景可接受 | 复杂查询优化困难 | 需要手写SQL,注意风格统一 |
| 选型结论 | 适合做后端底座 | 不适合业务报表多的系统 | 适合病历类系统 |
从结果来看,这套组合最大的好处是开发中几乎不会被框架本身卡住。所有精力都集中在业务逻辑上,而后期的运维排查也因为体系熟悉而变得顺畅。
3. 数据库设计:一张病历是怎么存进MySQL的
3.1 核心业务表梳理
数据库设计是整个项目的根基,后续几乎每一个接口都在跟这些表打交道。我们最终拆成了用户权限类、患者档案类、就诊记录类、病历业务类四大块。
| 表名 | 说明 | 核心字段 |
|---|---|---|
| sys_user | 系统用户 | id, username, password, dept_id, real_name, status |
| sys_role | 角色表 | id, role_name, role_key |
| sys_menu | 菜单权限表 | id, parent_id, menu_name, path, perms |
| sys_user_role | 用户角色关联 | user_id, role_id |
| med_patient | 患者档案 | patient_no, name, gender, birth_date, id_card_no, phone |
| med_visit | 就诊记录 | visit_no, patient_id, visit_type, doctor_id, dept_id, visit_time, status |
| med_record | 病历主表 | record_no, visit_id, patient_id, doctor_id, dept_id, record_type, content_json, content_html, record_status, version |
| med_diagnosis | 诊断表 | record_id, diagnosis_name, diagnosis_code, diagnosis_type, sort_no |
| med_prescription | 处方主表 | record_id, prescription_no, total_amount, status |
| med_prescription_item | 处方明细 | prescription_id, drug_name, dosage, spec, quantity, usage |
| med_order | 医嘱表 | record_id, order_content, order_type, status, execute_time |
| sys_operation_log | 操作日志 | user_id, operation_type, operation_content, ip, create_time |
最关键的是med_visit这张表。它是整个业务流程的枢纽:患者挂一次号生成一条就诊记录,所有病历、诊断、处方、医嘱都通过visit_id关联到同一次就诊上。这样一个患者多次就诊时,只需要按visit_id就能把每次就诊的所有信息完整抽出来。
3.2 病历文书的结构化和模板化存储
电子病历最核心的难点是记录内容既要有结构,又要保留医生自由书写的灵活性。比如“现病史”一段,有的医生写两句话,有的医生写一大段;同一家医院里,内科和外科的病历模板差异也很大。
如果直接把每个字段都设计成数据库的硬列,比如主诉一列、现病史一列、体格检查一列,遇到不同科室需求时就必须频繁改表结构,这不是好的方案。我们采用的思路是在病历主表中保留两个关键字段:
- content_json:按结构存放段落内容,每一段都有字段标识和内容,比如主诉、现病史、既往史、体格检查、辅助检查、初步诊断。
- content_html:保存渲染后的完整病历内容,用于打印和查看时直接展示。
医生在前端选择模板,填写各个区块,提交时后端先把整份病历渲染成HTML保存一份快照,再把结构化JSON也存一份。这样既保证了数据能按段落检索、做质控评分,也保证了最终打印出来的病历版式始终一致,不会被模板调整影响历史记录。
3.3 权限模型:多科室多角色的数据隔离
权限模型使用的是经典RBAC设计。用户挂角色,角色挂菜单和按钮权限,同时每个用户带着一个dept_id(科室)。所有病历查询接口都强制拼上数据权限条件。
比如门诊医生查“患者列表”,SQL会带一个减法条件,只看自己创建的记录;科室主任可以看到整个科室的记录;医务科的人则按全院范围查询。这个数据权限隔离一定要在SQL层面做,而不是查完全部数据后在Java内存里过滤,否则数据量一大接口就会快速变慢。
为了支持“强制不让普通医生批量导出患者信息”,我们在菜单权限之外又加了查询导出权限的单独判断。前端导出按钮的显示由按钮权限控制,后端接口再次校验权限,防止有人绕过前端直接调用导出接口。
3.4 索引设计和软删除经验
数据量毕竟会增长,索引设计不能省。以下几个索引是必须的:
- med_visit表:(patient_id)、visit_no唯一索引、(doctor_id, visit_time)联合索引。
- med_record表:(visit_id)、record_no唯一索引、(patient_id, record_status)联合索引。
- med_diagnosis表:(record_id)、(diagnosis_code)。
- sys_operation_log表:(user_id, create_time)。
另一个容易被忽视的点是医疗数据不能物理删除。我们在所有业务表都加了del_flag字段,删除操作实际执行update,只把状态标记为已删除。患者档案删除时还会校验是否存在关联的就诊记录,如果有,直接提示“该患者存在历史就诊记录,无法删除,只能将账号停用”。这样虽然多写了一些逻辑,但数据回溯时才不会丢掉关键病案。
4. 后端业务实现:从登录鉴权到病历文书生成
4.1 登录认证、JWT鉴权和操作留痕
后端模块按标准的Controller-Service-Mapper三层组织。登录接口负责校验用户名密码,成功后生成JWT令牌返回给前端。前端后续所有请求都在Header里带上这个令牌,后端拦截器统一解析并校验。
核心逻辑大致是这样的:
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
// 从Token中解析用户ID和角色信息
LoginUser loginUser = JwtUtil.parseToken(token);
if (loginUser == null) {
// 未登录或Token失效,直接返回401
response.setStatus(401);
return false;
}
// 通过ThreadLocal保存当前登录用户,供本次请求全程使用
UserContext.set(loginUser);
return true;
}
@Override
public void afterCompletion(...) {
// 请求结束后必须清理ThreadLocal,避免线程池复用导致数据串号
UserContext.clear();
}
}
这里有一个特别容易踩的坑:ThreadLocal不清理会导致用户信息串号。因为开发环境用的线程池是复用的,如果当前请求结束后不把ThreadLocal里的用户信息移掉,下一次请求可能拿到上一个用户的身份。我们上线前专门排查过这一类bug,最后确定的规范是每个接口的请求在afterCompletion阶段统一清理UserContext。
操作日志是另一个必须做的点。我们没有在每个业务方法里手工写日志代码,而是用了Spring AOP,定义了一个@OperationLog注解,标注在需要记录日志的Controller方法上,通过切面统一记录用户、模块、动作、请求参数、耗时和IP。
java复制@OperationLog(action = "创建病历", module = "病历管理")
@PostMapping("/record")
public Result saveRecord(@RequestBody RecordSaveDTO dto) {
return recordService.saveRecord(dto);
}
切面里记录操作日志时,会基于JWT里的用户信息和前端传来的幂等请求编号做关联,保证每次操作都能追到具体人。
4.2 病历文书生成与模板渲染的实现
病历文书的生成逻辑比普通表单复杂。因为不同科室有不同模板,同一种模板的不同区块又有不同的填写输入方式,有的区块是下拉框,有的区块是纯文本,有的区块需要动态插入药品或者检查项目。
我们的实现方式是把模板和内容分离。数据库里维护了一份科室病历模板配置表,每个模板用JSON结构定义区块:
json复制{
"templateName": "首次病程记录",
"sections": [
{ "key": "chiefComplaint", "label": "主诉", "type": "text", "required": true },
{ "key": "presentIllness", "label": "现病史", "type": "textarea", "required": true },
{ "key": "physicalExam", "label": "体格检查", "type": "textarea", "required": true },
{ "key": "diagnosis", "label": "初步诊断", "type": "diagnosis-selector", "required": true }
]
}
前端拿到这个模板JSON后,动态渲染对应的表单组件。提交时后端把填写内容存到med_record表的content_json字段,同时根据模板生成一份完整的HTML快照,存入content_html字段。这样电子病历在查看和打印时永远基于快照内容,不会被模板后续的调整影响。
在实现这一步时,我建议把模板版本也存进content_json里。比如模板的v2版本调整了诊断区块的样式,但历史病历必须保持当时的版式,模板版本字段就是用来控制“当前编辑用最新模板,历史查看用快照”。
4.3 事务控制:病历、诊断、处方一次提交全部入库
一个完整的门诊病历创建接口,往往会同时写入多张表:
- 往med_record表插入一条主记录。
- 往med_diagnosis表插入一到多条诊断记录。
- 往med_prescription表插入处方头记录。
- 往med_prescription_item表插入处方明细。
如果中间任何一步失败,整个操作必须回滚,否则会出现病历明明有数据但处方没存上的脏数据。这就必须在Service层的方法上加事务注解:
java复制@Transactional(rollbackFor = Exception.class)
public void saveFullRecord(RecordSaveDTO dto) {
Long recordId = recordMapper.insert(dto.buildRecord());
diagnosisMapper.insertBatch(dto.getDiagnosisList(), recordId);
Long prescriptionId = prescriptionMapper.insert(dto.buildPrescription());
prescriptionItemMapper.insertBatch(dto.getPrescriptionItems(), prescriptionId);
operationLogService.record("创建病历", dto.getVisitId());
}
用rollbackFor = Exception.class很重要,因为Spring默认只回滚运行时异常,如果代码里抛出的是自定义检查异常,不加这个参数事务不会回滚。这个细节我见过太多项目在这个地方出问题,建议直接形成规范写进团队的开发约定里。
4.4 并发写病历时的数据覆盖问题
医院档案科的人可能不会强调,但医生门诊高峰时是会同时开两个窗口处理患者的。更常见的情况是,同一个患者因为复诊或者转科,两条问诊记录同时被处理,如果系统没有做并发控制,后保存的内容就会覆盖先保存的内容。
解决方案是乐观锁。在med_record表增加version字段,每次提交时后端检查数据库里的version是否和提交时一致,如果一致则更新并将version加一,不一致说明有其他人在保存,直接提示“该病历已被其他医生修改,请刷新后重新编辑”。
sql复制UPDATE med_record
SET content_json = ?, content_html = ?, version = version + 1
WHERE id = ? AND version = ?
影响行数为0时,说明更新失败,此时前端会收到一个明确提示,而不是数据库层无感知覆盖。这样医生的录入体验虽然有轻微打断,但保证了病历的完整性和可信度。病历质控人员看到版本冲突也会提醒医生确认数据,这个逻辑必须保留。
5. 前端Vue3实现:医生真正愿意用的界面怎么搭
5.1 工程搭建和开发环境配置
前端用Vite创建Vue3项目,配合Element Plus作为基础组件库,效率非常高。工程里按功能模块划分目录,比如views下分patient(患者管理)、record(病历书写)、prescription(处方管理)、statistics(统计报表)目录,store放状态管理模块,api下面放所有接口封装。
开发环境的跨域问题通过Vite的proxy配置解决,前后端分离后联调很顺畅:
javascript复制// vite.config.js
export default defineConfig({
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '/api')
}
}
}
})
5.2 Axios封装和请求拦截器设计
接口请求这块必须封装统一,否则几百个页面每处都各自处理错误提示会很混乱。我们对Axios做了统一封装:
javascript复制import axios from 'axios'
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = token
}
return config
})
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
if (res.code === 401) {
// token失效,清空登录信息,跳转登录页
}
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
error => {
// 统一处理网络错误和超时
return Promise.reject(error)
}
)
这样做的核心好处是后端返回统一封装格式,前端只用关心业务处理,不用在每处接口调用里重复写错误分支。token失效统一跳转也避免了用户操作到一半突然看不了数据的问题。
5.3 病历录入编辑器与模板化组件
病历书写页面是整个系统里最复杂的单页。医生需要在一个页面内完成主诉、现病史、既往史、体格检查、诊断、处方等多个区块的填写。我们把它做成了基于区块动态渲染的编辑器界面。
前端通过v-for遍历模板JSON中的sections配置,根据每个区块的type字段渲染对应的组件。比如type为text时渲染输入框,textarea时渲染多行文本,diagnosis-selector时渲染一个带搜索按钮的诊断选择组件,点击后弹出诊断检索框,选中后回填到区块。
考虑到医生书写的连续性,编辑器还加了一个“暂存”功能:每30秒自动把当前内容保存到本地草稿表。这样即使医生在写病历过程中被叫走去处理其他患者,回来也不会丢失内容。这个细节非常实际,有些系统上线后被医生吐槽难用,很大程度就是因为没有考虑真实门诊的干扰环境。
5.4 患者列表与统计报表页面
患者列表页是整个系统的“门面”,医生每天打开系统后首先看到的就是今天需要处理的患者列表。这个页面按就诊时间倒序排列,支持按病历状态筛选,比如“待书写”“已草稿”“已提交”“已归档”。
统计报表页面向医院管理层展示门诊量、病历书写及时率、病历归档率等核心指标。我们用开源的ECharts库绘制图表,后端提供数据的聚合查询接口,前端拿到数据后渲染成柱状图、折线图和饼图。实现这一层时要注意图表数据接口的性能,医院要求的是半天门诊量统计,SQL里用到了日期函数和count聚合,配合索引后毫秒级返回,体验良好。
6. 联调部署阶段的坑:时区、跨域、大字段一个都别漏
6.1 前后端联调的跨域问题
本地开发时Vite的proxy解决了跨域,但生产环境如果前端和后端不在同一个域名下,就必须通过Nginx统一配置反向代理。我们当时的部署架构是:前端静态文件由Nginx托管,Nginx把以/api开头的请求反向代理到后端服务。
nginx复制server {
listen 80;
server_name _;
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这个配置里有个小坑:try_files配置必须把前端history路由的兜底加上,否则用户直接访问www.xx.com/record时,Nginx找不到这个物理路径会返回404,必须让它回退到index.html交给前端路由处理。
6.2 MySQL时区与日期时间字段的坑
联调阶段我们遇见过一个非常隐蔽的问题:后端保存的创建时间和数据库存储的时间相差八个小时。排查后发现是MySQL连接串没有显式配置时区,而数据库服务器和应用的默认时区不一致。
最终在所有环境统一使用了明确的连接串配置:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
建议从一开始就统一:数据库连接串显式指定时区,前端和后端接收的时间字段统一使用时间戳或者格式化的字符串,避免在JavaScript的Date对象上做二次转换。医疗系统的时间记录还是准确为妙,修改记录时如果时间差八小时,质控人员根本没法判断操作时间。
6.3 大文本字段导致的慢查询排查
病历列表页上线后出现过一次性能问题:列表接口响应要两三秒。通过打印执行的SQL发现,查询列表时把med_record表的content_html这个大字段也一并SELECT出来了,而列表页根本不需要展示完整病历内容,只要标题、医生、日期和状态。
优化方案很直接:列表查询的SQL只返回需要的字段,详情接口再单独查询content_html和content_json。同时给列表查询加了一条强制索引,确保排序永远走(patient_id, record_status)联合索引。
如果你在做类似系统时遇到列表变慢,第一步就是先看SQL有没有把大字段拖进来,这是性价比最高的优化。数据库本身没问题的情况下,这种优化能把接口从秒级降到毫秒级。
6.4 事务失效的排查记录
还有一次典型事故:批量导入历史病历时,处理了前五百条后报错,结果这五百条数据一条都没进库。检查后发现导入方法内部调用了同一个类的另一个方法,而那个内部方法是private的。Spring的事务是基于代理机制的,同类内部调用不会经过代理,所以事务注解根本没生效,异常发生时无法触发回滚。
正确的做法是把导入逻辑单独抽到一个Service类里,通过注入的方式调用,或者在方法内部通过Spring上下文获取代理对象再调用。我们最终选择了拆分Service结构,把批量导入独立成一个BatchImportService,事务边界清晰,后续维护也更方便。
7. 源码交付与二次开发注意点
7.1 目录结构与代码规范
这套系统的后端工程按单体结构组织,模块边界通过maven的包分区来保证。后端源码目录大致是:
text复制hospital-server
├── common // 公共返回体、异常处理、工具类
├── config // 全局配置、拦截器注册、跨域配置
├── security // JWT拦截器、登录认证
├── module
│ ├── system // 用户、角色、菜单、部门
│ ├── patient // 患者档案
│ ├── visit // 就诊管理
│ ├── record // 病历管理、模板管理、质控
│ ├── prescription// 处方和药品管理
│ └── statistics // 统计报表
└── framework // AOP日志、数据权限插件等
前端工程同样按业务模块划分。二次开发时如果增加一个体检模块,就在module下新增一个跟现有模块平行的包,尽量不侵入病历主流程的代码。这个约定帮助我们在接手后续需求时,不会因为改动一个功能而影响另一个功能的稳定性。
7.2 数据字典与字段扩展建议
项目中所有涉及类型枚举的地方,比如就诊类型、病历状态、性别、诊断类型,都通过sys_dict数据字典表统一管理。前端下拉框选项也是从数据字典接口动态获取的,而不是写死在代码里。
新增字典项只需要在管理后台插入一条记录,不需要改前端代码和后端枚举,对维护人员非常友好。如果在二次开发中要增加新的病历类型,第一步也是配字典,第二步是配置模板,第三步才是考虑是否改表结构。坚持这个顺序,系统基本不会因为频繁变更而失控。
7.3 从病历系统走向完整信息平台
这套系统跑通后,后续很自然会往更完整的平台方向扩展:接入检验检查报告回传、对接挂号收费系统、提供患者端小程序查看报告和既往病历、增加科研数据检索能力。架构上只要保持med_visit表和med_record表作为核心枢纽不变,其他系统都通过就诊号对接,扩展起来会平滑很多。
我个人做这类系统最大的体会是:病历系统表面是几组CRUD接口加前端表格,实际上背后是一整套围绕患者就诊链路的业务闭环。把核心主流程走通容易,难的是把每个边界场景处理好——数据版本冲突、操作留痕、状态流转、大字段性能、权限隔离。这些在实际项目中都会被反复问到,也是这套源码真正值钱的地方。按本文的顺序把业务、表结构、后端、前端、部署一条链想清楚,再动手写代码会顺手很多。
