
做社区医院管理系统这类项目我前后接触过不少说句实话网上流传的源码质量参差不齐很多拿到手根本跑不起来。要么MySQL版本跟驱动不匹配要么Node依赖装得乱七八糟要么MyBatis映射文件路径配错一路飘红。这篇文章把我实际完成一个基于SpringBootVue的社区医院管理系统的全过程复盘一遍从数据库建模到后端接口开发再到前端页面联调和最终打包部署把能直接用的代码片段、关键配置和踩坑记录都整理出来。适合正在做毕业设计的学生也适合刚工作想拿一个完整项目练手、把SpringBootMyBatis这套组合彻底跑通的开发者。社区医院的业务其实不算复杂核心就是患者建档、医生排班、挂号就诊、处方收费、药品库存这几条主线。难的是把这几条线串起来形成一个能跑通闭环的业务系统。而恰恰是这种麻雀虽小五脏俱全的项目能让你把一个BS架构项目的全部标准动作练个遍基于角色的登录鉴权、多表联查的报表统计、前后端分离下的接口规范、事务处理、异常拦截甚至部署上线。把这一整套流程走完SpringBootVue这套主流技术栈的核心打法基本就心里有数了。1. 项目需求解析与整体设计思路1.1 社区医院的核心业务到底是什么在动手写代码之前先得把业务捋清楚。社区医院和大型三甲医院最大的区别在于规模小但环节全——患者来了要挂号挂了号要候诊医生要写病历、开处方药房要发药、扣库存收费处要结账院长还想看每天的接诊量和收入。这些环节拆开看都不难但连成一个完整流程就涉及多张表的联动和数据一致性问题。我习惯先把角色和用例梳理一遍。这个系统里至少有四类用户系统管理员、医生、护士/收费员、药剂师。管理员管科室、医生、药品这些基础数据医生处理自己的排班和接诊写病历开处方收费员操作挂号收费和处方结算药剂师看药品库存、处理出入库。每个角色看到的页面、能操作的功能都不一样这就决定了后端必须有完整的鉴权控制前端也要有对应的路由权限拦截。整个系统的核心流程可以浓缩成一句话患者挂号后进入医生接诊环节医生开具诊断和处方收费完成后药房发药并扣减库存。围绕这条主流程再辐射出患者病历管理、药品库存预警、科室工作量统计等辅助功能。我做需求的时候会把这个主流程画成一张简单的状态流转图挂号的单子从待就诊到就诊中再到已完成每一步的状态变更都是有前后置条件的代码里就必须把这些校验写到位。1.2 技术选型为什么这套组合能打技术栈选SpringBootVueMySQLMyBatis说白了是当前Java技术栈里性价比最高的一套组合。SpringBoot解决的是框架配置繁琐的问题内嵌Tomcat让部署变得极其简单一个jar包扔到服务器上就能跑。MyBatis虽然轻量但对SQL有完全的控制力适合这种需要写复杂多表联查查询的场景比JPA那种自动生成SQL的方式更直观、更可控。前端选Vue而不是传统JSP或者FreeMarker模板核心原因是前后端分离的开发体验确实好。后端同学专心写接口前端同学专心写页面两边通过JSON格式的数据契约协作互不干扰。Vue本身的响应式机制和组件化开发方式对于类似表单渲染、列表展示这种大量交互的页面来说开发效率比jQuery时代高了不止一个量级。MySQL更不用多说社区医院这种量级的数据并发MySQL完全够用而且部署和维护成本极低学习和排查问题的资料也最多。这套组合选型可能不够高级但在真实项目中稳定、易维护、有人能接手比追新求变更重要。1.3 功能模块的划分与优先级排序我的习惯是先把功能模块清单列出来标好优先级再动手。社区医院管理系统我拆成了八个模块用户登录认证与管理、科室管理、医生管理、患者管理、挂号管理、就诊病历管理、处方与收费管理、药品库存管理外加一个数据统计面板。开发顺序上一定要先做基础数据和登录认证再做核心业务流。因为患者、医生、科室这些基础数据没有挂号功能根本没法测试没有登录认证所有接口裸奔后面加权限等于返工。我实际开发的顺序是数据库表结构设计——后端登录认证和用户管理——科室和医生管理——患者管理——挂号——病历处方——药品库存——统计报表。这个顺序的好处是每完成一个阶段都能独立测试不用等到全部做完才验证。2. 数据库设计系统的地基工程2.1 核心表结构和字段设计思路数据库设计是这个项目最关键的一步后面所有业务逻辑都建立在表结构之上。设计得合理写代码是顺水推舟设计得草率后面查数据能让你怀疑人生。我把核心表列一下每张表说清楚关键字段和设计理由。用户表是系统的根基字段不算多但每个都讲究。id主键自增不用多说username必须唯一password存的是加密后的密文千万不能明文存储role字段定义了用户的角色类型我用数字表示1是管理员2是医生3是收费员4是药剂师。这样设计的好处是角色判断在代码里就是一个等值比较非常轻量。患者表需要体现社区医院的特点病历号、姓名、性别、身份证号、电话、过敏史。病历号不要用随机数推荐用时间戳加序列号拼接生成保证唯一且可读。过敏史字段容易被忽略但在处方开药时过敏药物必须拦截这个字段能直接支撑药品安全校验逻辑。科室表、医生表、药品表属于基础数据表字段设计相对直观。医生表通过user_id关联用户表这样医生的账户和基本信息是一体的department_id关联科室表后面做科室排班和统计都要用。药品表里有个容易忽略但重要的字段是药品编码我建议用规则编码比如字母加数字方便检索和扫码。我准备用病历表来示例如何设计一个关联多表的业务表。medical_record表的核心字段包括关联的挂号id、患者id、医生id以及患者的自述、诊断结论、医生建议。注意病历表只存描述性信息药品明细放到独立的处方表里因为一个诊断可能对应多种药品这个一对多的关系不能塞在同一张表里。2.2 处方与库存设计的核心细节处方表是整个系统业务逻辑最密集的一张表。一条处方记录包含病历id、患者id、药品id、单次剂量、用药频次、开具天数、总数量、总金额。这些字段全部是为了支撑收费和库存扣减的逻辑总数量由频次乘以天数计算得出金额由数量乘以药品单价得出。库存扣减是容易出事故的地方。我的设计是在药品表直接维护stock_count字段开处方时先判断库存是否充足如果不足直接拒绝开方。扣库存的动作必须和创建处方在同一个数据库事务里否则会出现处方开了但库存没扣的脏数据。这里我踩过坑后面在排查章节详细说。另外我做了一张charge_record收费记录表每笔收费都有一条记录关联挂号id或处方id。为什么要单独拆一张表因为后续做每日营收统计时直接从收费记录表聚合数据远比从处方表反查效率高而且收费记录是append-only的天然适合做统计。2.3 建表SQL的关键写法示例整理一个简化版的建表脚本实际项目中字段可以按需扩充。关键点我都写在注释里CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL COMMENT 角色1管理员2医生3收费员4药剂师, phone varchar(20) DEFAULT NULL, status tinyint(4) DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; CREATE TABLE patient ( id bigint(20) NOT NULL AUTO_INCREMENT, patient_no varchar(30) NOT NULL COMMENT 病历号, name varchar(50) NOT NULL, gender tinyint(4) DEFAULT NULL COMMENT 1男 2女, id_card varchar(18) DEFAULT NULL, phone varchar(20) DEFAULT NULL, birthday date DEFAULT NULL, allergy_history varchar(500) DEFAULT NULL COMMENT 过敏史, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_patient_no (patient_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者档案表;药品表和处方的建表逻辑我再补一段这段是后面业务实现的核心CREATE TABLE drug ( id bigint(20) NOT NULL AUTO_INCREMENT, drug_code varchar(30) NOT NULL COMMENT 药品编码, drug_name varchar(100) NOT NULL COMMENT 药品名称, specification varchar(100) DEFAULT NULL COMMENT 规格, unit varchar(20) DEFAULT NULL COMMENT 单位盒/瓶/袋, price decimal(10,2) NOT NULL COMMENT 单价, stock_count int(11) NOT NULL DEFAULT 0 COMMENT 当前库存, manufacturer varchar(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_drug_code (drug_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT药品信息表;3. 后端核心实现SpringBootMyBatis实战3.1 项目结构规划与统一响应封装后端工程结构我习惯用标准的controller-service-mapper三层架构加上common包放公共类。一个清晰的目录结构能省掉大量找文件的烦恼尤其是后期代码量上来之后。我的目录大概长这样src/main/java/com/chms ├── ChmsApplication.java # 启动类 ├── common/ │ ├── Result.java # 统一响应结果 │ ├── ResultCode.java # 状态码枚举 │ ├── PageResult.java # 分页结果 │ └── BusinessException.java # 业务异常 ├── config/ │ ├── WebMvcConfig.java # 拦截器注册 │ └── JwtInterceptor.java # JWT鉴权拦截器 ├── controller/ # 接口层 ├── service/ # 业务层 ├── mapper/ # MyBatis持久层 └── entity/ # 实体类统一响应结果这个设计强烈建议每个项目都做。如果每个接口返回的数据格式都不统一前端处理起来就是灾难性。我的Result类设计如下public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }前端axios请求拦截器统一处理这个结构只要code不等于200就直接弹message提示不需要每个接口都写一遍错误判断逻辑代码顿时清爽很多。3.2 JWT登录鉴权与拦截器配置登录认证我用JWT来做的。流程很简单用户提交用户名密码后端校验通过后生成token返回前端前端把token存在localStorage里每次请求在header里带上token后端拦截器验证token的合法性并从中解析出用户id和角色信息。JWT生成的核心代码封装在工具类里public class JwtUtils { private static final String SECRET chms-secret-key-2024; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; // 24小时 public static String generateToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器里做的事情不复杂但要逻辑严密。先放行登录接口其余接口校验token是否存在且合法。解析出来的userId和role放进request的attribute里后续controller里如果需要当前登录人信息直接从request取就行不用每次都在接口参数里传用户id。3.3 挂号流程的业务逻辑实现挂号是这个系统里最核心的线上线下交互业务。挂号时要做的事很多校验患者存在、校验医生当天是否有排班、检查同一患者是否重复挂号、生成挂号单号、计算挂号费用。这些逻辑串起来就是一条完整事务每个环节出错都要回滚。我用Registrationservice里的方法来做示例Service public class RegistrationServiceImpl implements RegistrationService { Override Transactional(rollbackFor Exception.class) public Registration createRegistration(RegistrationDTO dto) { // 1. 校验患者存在 Patient patient patientMapper.selectById(dto.getPatientId()); if (patient null) { throw new BusinessException(患者不存在); } // 2. 校验医生当天的出诊排班 DoctorSchedule schedule scheduleMapper.selectByDoctorAndDate( dto.getDoctorId(), dto.getVisitDate()); if (schedule null) { throw new BusinessException(医生当天没有排班); } // 3. 防止同一天重复挂号 int count registrationMapper.countByPatientAndDate( dto.getPatientId(), dto.getVisitDate()); if (count 0) { throw new BusinessException(该患者当天已挂号); } // 4. 生成挂号单号并入库 Registration reg new Registration(); reg.setRegNo(REG System.currentTimeMillis()); reg.setPatientId(dto.getPatientId()); reg.setDoctorId(dto.getDoctorId()); // ... 其余字段赋值 registrationMapper.insert(reg); return reg; } }这段代码里有几个值得注意的细节。生成挂号单号我用时间戳拼接简单且不容易重复。如果要更严谨可以用Redis生成一天内的自增序列但社区医院并发量不大时间戳方案就够了。Transactional注解必须指定rollbackFor否则遇到RuntimeException之外的异常事务不会回滚这个坑我见不少人踩过。3.4 MyBatis多表联查与动态SQL实践处方的查询往往需要关联患者、药品、病历三张表用简单的SELECT就能查出来但动态条件会让SQL变得复杂。MyBatis的动态SQL威力在这里体现得淋漓尽致。我写一个药品库存查询的Mapper来举例支持按药品名模糊搜索和按库存预警阈值过滤select idselectDrugPage resultTypecom.chms.entity.Drug SELECT d.* FROM drug d where if testkeyword ! null and keyword ! AND d.drug_name LIKE CONCAT(%, #{keyword}, %) /if if testminStock ! null AND d.stock_count lt; #{minStock} /if /where ORDER BY d.id DESC /selectXML里写SQL有个老生常谈的坑小于号在XML文档里会被解析成标签开始符号必须转义成。我第一次写库存预警时被这个错误折磨了半天报错信息绕来绕去最后才排查到是XML转义问题。MyBatis分页我推荐直接用PageHelper插件引入依赖后一个PageHelper.startPage()方法就搞定分页查询返回PageInfo对象里自动带有总条数和分页数据。但要注意一个坑PageHelper的Page对象会静态绑定到下一次查询如果startPage和后面的查询之间隔了其他SQL执行分页就会失效。最好的用法是startPage和Mapper查询之间不放任何其他数据库操作。4. 前端设计Vue从搭建到联调4.1 Vue项目初始化与路由整合前端这块我用的是Vue 2.6加Element UI这套组合虽然不算新但稳定、资料多、踩坑答案全毕业设计和中小型系统都够用。Vue 3加Element Plus当然也能做但如果你是第一次做完整项目我建议还是按Vue 2来省下的折腾时间拿去写功能更实在。创建项目我推荐用Vue CLInpm install -g vue/cli之后一行vue create chms-web选择手动配置勾选Router和Vuex。路由设计需要跟权限角色对应起来我的路由表大致思路是登录页是独立路由主布局Layout下面挂各个功能页。这部分有一个容易踩坑的地方路由跳转时页面如果刷新Vuex里的用户信息就丢了。我的处理办法是在刷新时重新从localStorage里的token解析用户信息虽然不算最优方案但够用。路由守卫必须写不然后端的鉴权等于形同虚设。vue-router的beforeEach钩子里做两件事判断是否有token没有就跳登录页判断当前用户角色是否能访问对应路由不能就提示无权限。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.path /login) { next(/) } else { next() } })这里我踩过的坑是动态路由权限如果做不好刷新页面后路由直接404。所以要么把权限判断放到组件内的v-if和菜单渲染上要么在刷新时重新加载一次动态路由。为了降低复杂度我最终的方案是路由全部静态注册菜单和按钮级别控制用v-if配合角色判断来实现简单可靠。4.2 Axios封装与接口对接技巧Axios封装是我的固定动作绝对不在每个页面里直接写axios.get。统一封装的优点是统一处理baseURL、统一注入token、统一拦截错误响应并弹出提示。// request.js import axios from axios import { Message } from element-ui import router from ../router const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 }) // 请求拦截器注入token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { Message.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request这个封装的精巧之处在于响应拦截器直接把res.data返回出来调用方拿到的就是后端返回的业务数据不需要再叠一层res.code判断。前后端联调还有个逃不掉的坑——跨域。开发阶段我直接在SpringBoot的WebMvcConfigurer里配置了CORS映射允许前端开发服务器发起的跨域请求。等部署阶段前后端打好包放同一个端口下跨域问题就彻底不存在了。4.3 核心页面组件的实现拆解前端页面我挑两个有代表性的讲讲挂号登记页面和就诊工作台。挂号登记页面是操作最频繁的界面交互上要有患者查询、医生按科室筛选、当天排班展示。组件布局是左中右结构左边患者查询区中间科室和医生列表右边排班时间轴。患者查询走后端模糊搜索接口搜索结果点击后自动填充患者信息卡片。医生列表按科室tab切换选中医生后右边加载当天出诊时段再选时间段点击确认挂号就提交了。我在挂号页里加了重复挂号的即时拦截选中患者和医生后前端先调用一次查询今日挂号记录接口如果已经有记录就禁用确认按钮。虽然后端也有校验但前端提前拦截能避免用户操作一半被弹窗打断的糟糕体验。就诊工作台是医生用的核心页面数据加载逻辑是当天待就诊队列、当前就诊患者信息、病历填写表单、处方添加区。前端最费心思的地方是处方明细的表格编辑添加药品时要用dialog弹窗搜索药品并选择选中后自动带出单价和库存再填写频次和天数前端实时计算总价。el-table :dataprescriptionItems el-table-column propdrugName label药品名称 / el-table-column propspecification label规格 width120 / el-table-column label数量 template slot-scopescope el-input-number v-modelscope.row.quantity :min1 sizemini / /template /el-table-column el-table-column propprice label单价 width90 / el-table-column label金额 template slot-scopescope {{ (scope.row.quantity * scope.row.price).toFixed(2) }} /template /el-table-column /el-table页面联调过程中最常见的错误是字段名对不上。后端的实体字段是驼峰命名如果JSON没做下划线转驼峰处理前端拿到手的字段就可能是create_time而不是createTime。我在SpringBoot的配置里统一开启了驼峰映射保证前后端字段风格一致省掉大量没必要的沟通成本。5. 高频疑难问题与排查定位实录5.1 跨域与请求配置导致的接口不通前后端分离项目第一个坎就是跨域。开发环境下前后端跑在不同端口axios请求会被浏览器拦截。报错信息通常会出现在浏览器控制台提示CORS开头的一串英文。排查思路分两步先确认后端是否配置了跨域再确认请求头是否被预检请求拦下。后端配置CORS我放在统一的配置类里Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }另外要注意的是如果配置了拦截器预检请求OPTIONS也会被拦截器拦截。我的处理办法是在拦截器里直接放行OPTIONS请求if (OPTIONS.equals(request.getMethod())) { return true; }。这个细节容易忽略忽略之后的表现是跨域配置看着没问题但请求依然一直失败。5.2 MyBatis映射与SQL限流的经典坑我碰到过的一个高频错误是Invalid bound statement (not found)这个报错。出现这个错误基本可以断定是Mapper接口和Mapper.xml没有正确绑定。检查点有三个第一Mapper接口的包路径和XML的namespace是否完全一致第二接口方法名和XML中select/update的id是否对应第三application.yml里是否配置了mapper-locations。一个容易踩的细节是XML文件放错位置。如果放在src/main/java目录下而非resources目录SpringBoot默认不会扫描到这些XML。解决方法是把XML放在resources/mapper目录下并在配置里指定mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.chms.entity configuration: map-underscore-to-camel-case: true另外要格外提醒一点千万别在循环里逐条执行SQL。我曾经在处理批量开处方时循环调用单一插入方法几十条记录要跑好几秒。后来改用MyBatis的foreach标签批量插入性能提升了十倍不止。批量操作的代码可读性稍微差点但性能收益在业务里是实打实的。5.3 日期时间与时分秒的显示陷阱MySQL的datetime类型和Java的LocalDateTime对接时如果处理不好前端显示会出现一堆19位时间戳或者比预期少8小时的问题。根因是时区配置。MySQL连接串里必须要指定serverTimezone我统一用Asia/Shanghaispring: datasource: url: jdbc:mysql://localhost:3306/chms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse另外后端返回给前端的时间格式我习惯在实体类的日期字段上加Jackson格式化注解。顶层全局配置更省事spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai如果遇到前端显示的时间差8小时优先查两层数据库连接串是否指定了时区以及Jackson的time-zone是否设置。这两层都对了基本不会出问题。5.4 常见问题速查表我把这个项目里遇到的高频问题按症状归类直接列成排查表帮助你快速定位问题不走弯路症状可能原因排查方向前端请求报401token过期或未携带检查token是否存在于localStorage查看请求头是否注入接口报500SQL语句错误或空指针查看后端日志堆栈定位controller和service层分页数据不准确PageHelper位置错误确保startPage后紧跟第一个查询前端页面白屏报错组件引入错误或路由未匹配打开浏览器控制台查看具体报错排查import路径药品库存成负数扣减逻辑无事务检查createPrescription方法是否加了Transactional时间差8小时时区未配置检查MySQL连接串和Jackson配置6. 构建部署与后续扩展建议6.1 打包部署的前后端整合策略开发完成后的部署我推荐一种最省事的方式前端打包成静态文件直接放到SpringBoot的resources/static目录下前后端一体化部署。这样只需要启动一个Java进程不用操心Nginx配置对初学阶段的部署体验极其友好。前端打包有个关键坑路由模式必须使用hash模式而非history模式。如果用了history模式前端路由比如/admin在直接访问时会因为找不到对应后端接口而返回404。改成hash模式后路由会带上#号刷新和直接访问都不会出问题。打包命令也很简单npm run build生成到dist目录然后把dist目录下所有文件复制到后端项目的src/main/resources/static目录。重新打包后端工程mvn clean package -DskipTests生成的jar包直接运行java -jar chms-server.jar启动后访问http://localhost:8080登录页就能正常打开了。为了方便远程部署我还加了生产环境的MySQL配置账号密码提前在数据库里初始化好。6.2 上线前必须检查的安全项部署上线前有几件安全事项不能偷懒。第一所有用户的初始密码必须强制修改我在用户管理里加了首次登录强制改密逻辑不改成新密码不允许访问功能页面。第二前端页面对隐藏字段做了校验但后端必须重复校验比如管理员删除医生时必须检查该医生名下是否有未完成的就诊记录前端可以做二次确认但最终防线一定在后端。第三管理员的登录日志要记录登录时间和IP系统维护时排查问题能省很多时间。数据库备份我建议每天做一次定时备份至少保留最近7天的备份文件。社区医院数据量不大直接用mysqldump就足够mysqldump -u root -p chms /backup/chms_$(date %Y%m%d).sql万一线上出了数据问题至少有备份能恢复。6.3 个人实操体会与扩展方向现在回头看这个系统的代码量不算大但它把很多软件开发需要的基本功完整串了一遍。我对项目的最终体会是一个系统能不能跑得顺往往不是某个技术难点没攻克而是那些看起来毫不起眼的细节——时间格式统一、异常处理完整、事务边界清晰、接口返回格式一致——真正决定了系统的稳定性和可维护性。这些基本功在每一次写代码时下意识地做好后面上线维护会无比省心。如果后续想让这个系统更完整一些可以从几个方向扩展引入Redis缓存科室列表和字典数据减少数据库压力接入在线支付模拟流程让收费环节更完整增加药品近效期预警和批次管理让库存管理更细粒度前端引入ECharts做更丰富的业务数据可视化展示比如月度营收趋势、科室接诊量排行。这系统就是个基础架子只要地基打得稳往上加层的成本很低。做这种项目最大的收获其实是在完成的过程中亲手踩一遍这些坑再亲手把它们一一填平。