简介:面向小型诊所、医疗信息化初学者与开发者,这份简化版商业HIS系统源码包提供了轻量级的医院信息管理解决方案,涵盖病历管理、挂号、药品库存、收费结算、医生排班与统计报表等核心业务模块。压缩包共451个文件、约7.05MB,以C#代码(136个cs)、ASP.NET页面与处理程序(49个aspx、32个ashx)、前端脚本和样式(45个js、31个css)为主体,辅以png/jpg等界面素材、doc说明文档、Excel示例数据及配置文档,结构上清晰呈现了Web层、业务逻辑与资源配置的关系。资源中还可看到Nso.txt按日期生成的历史日志,以及Contract_Web等文件,适合用来拆解学习HIS系统的工程组织方式。目前已有285人学习下载,适合作为课程设计、毕业设计或小型诊所信息化建设的参考起点。
1. 一个社区诊所从 Excel 到 HIS:这套系统究竟解决了什么
我见过太多正在用 Excel 记账的社区诊所:患者资料散在好几个表格里,挂号靠手写,药品库存月底一对才发现少了十几盒。某天一个做诊所信息化的朋友丢给我这个「超级简单版本商业HIS系统.rar」,说这是他从某个外包项目里扒出来的基础版,让我帮他看看怎么二次开发。解压后跑起来,我意识到这套 HIS 系统最难得的地方在于——它把挂号、收费、药房、报表这些诊所日常流程压缩成了一个能被普通开发者吃透的闭环。它没有三甲医院那种巨型软件的复杂度,而是保留了商业 HIS 的核心业务逻辑,适合两类人:一类是接诊所或小医院外包的开发者,想找一个能快速改造成产品的低起点;另一类是想弄懂医疗业务系统里挂号收费流程的学生或刚转行的后端工程师。这套系统能让你在一个晚上跑通,再用一周时间改出自己的版本。
2. 技术选型与整体架构:一个能跑商业项目的 J2EE 骨架
2.1 技术栈为什么这么选
这套系统走的是典型的 J2EE 单体应用路线,没有微服务,没有容器编排,甚至连 Redis 都没用。后端基于 Spring Boot 2.x 搭建,持久层用 MyBatis Plus 简化 SQL 操作,数据库是 MySQL 5.7,前端用的是 Vue 2 + Element UI,构建后的静态资源直接打进 Spring Boot 的src/main/resources/static目录里。这么做的直接好处是部署极其简单:一个java -jar命令就能同时拉起后端和前端页面,不用单独配 Nginx,也不用维护两个进程。
我猜设计这套系统的开发者当时接的是中小诊所的活儿,客户预算有限,IT 基础设施可能就一台老 Windows 服务器。这种环境下,单体架构反而比微服务更可靠。登录认证用的是 Session,省掉了 JWT 的配置和 token 刷新问题。文件结构上,后端按 controller、service、mapper、entity 分包,前端页面按模块存放在 static 目录下的 html 文件里,没有用 Webpack 做复杂构建,直接改完刷新就能看到效果。
2.2 目录结构里的学问
把 rar 包解压后,整个工程的目录结构大概是这样的:
his-simple/ ├── pom.xml ├── src/main/java/com/his/ │ ├── HisApplication.java │ ├── config/ │ ├── controller/ │ ├── service/ │ ├── mapper/ │ ├── entity/ │ └── common/ ├── src/main/resources/ │ ├── application.yml │ ├── mapper/ │ └── static/ │ ├── index.html │ ├── login.html │ ├── css/ │ └── js/ └── sql/ └── his_init.sql注意sql目录单独放在工程根目录,不在resources下面,这提醒我们初始化数据库时要先手动执行这个脚本。common包里通常是统一的返回对象Result和异常处理,controller 层所有接口都返回这个对象,前端通过code字段判断成功失败。
2.3 关键数据表设计
医疗业务系统最核心的是数据表的关系。这套系统里,患者表和挂号记录表是一对多,挂号记录表和处方表是一对一,药品表和处方明细表是多对多。我梳理了几张核心表的字段,理解这些字段是后续改造的基础。
patient表存患者档案:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(32) | 患者姓名 |
| id_card | varchar(18) | 身份证号,唯一索引 |
| phone | varchar(11) | 联系电话 |
| address | varchar(200) | 住址 |
register表存挂号记录:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| register_no | varchar(32) | 挂号单号,格式如 R20250210001 |
| patient_id | bigint | 关联患者 id |
| dept_id | bigint | 科室 id |
| doctor_id | bigint | 医生 id |
| status | varchar(16) | WAIT / FINISH / CANCEL |
| create_time | datetime | 挂号时间 |
drug表存药品和库存,注意它把单价和库存放在同一张表里,对于小诊所来说不需要独立的库存流水表,直接在更新时扣减就行。
CREATE TABLE `drug` ( `id` bigint NOT NULL AUTO_INCREMENT, `drug_code` varchar(32) NOT NULL COMMENT '药品编码', `name` varchar(64) NOT NULL COMMENT '药品名称', `spec` varchar(64) DEFAULT '' COMMENT '规格', `unit_price` decimal(10,2) NOT NULL COMMENT '单价', `stock_num` int NOT NULL DEFAULT 0 COMMENT '库存数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这套表设计没有做过度范式化,比如药品和科室直接存 ID 而不是存冗余名称,但在register表里保留了dept_id和doctor_id两个字段,查询时要做 join。理解这个结构之后,你会发现业务逻辑的代码量其实很小,大部分工作量都在处理这几个表之间的关系。
3. 核心模块拆解:一次挂号收费的数据链路
3.1 登录与拦截器:五秒钟搞定的权限控制
登录接口是整个系统的入口。前端登录页把用户名密码提交到/api/login,后端校验通过后把用户对象放进 Session,同时给前端返回用户角色。我见过很多中小系统在这个环节过度设计,这套系统反而很务实,只区分管理员和操作员两种角色,权限控制靠一个拦截器解决。
@PostMapping("/api/login") public Result login(String username, String password, HttpSession session) { // 注意:密码使用了 MD5 加密存储,演示项目够用 // 生产环境建议改成 BCrypt,并加入验证码校验 User user = userMapper.findByUsernameAndPassword( username, DigestUtils.md5DigestAsHex(password.getBytes())); if (user == null) { return Result.error("账号或密码错误"); } session.setAttribute("loginUser", user); return Result.success(user); }登录接口的返回值里有code字段,前端拿到后判断是否跳转。拦截器做的事更简单,在preHandle里检查 Session 里有没有loginUser,没有就重定向到登录页。WebMvcConfig里注册拦截器时,记得把/api/login和静态资源路径排除掉,否则会出现登录页自己都访问不了的尴尬情况。
3.2 挂号逻辑:先建档案再出单据
挂号这个动作听起来简单,实际上涉及两个表的数据变更。第一次来的患者要先建档案,老患者直接选择科室和医生就能挂号。这套系统的RegisterService里把这两步封装在同一个事务里,避免出现患者建了档案但挂号失败的数据脏状态。
@Transactional(rollbackFor = Exception.class) public Result createRegister(RegisterVO vo) { // 1. 查询是否已有患者档案 Patient patient = patientMapper.selectByIdCard(vo.getIdCard()); // 2. 没有档案则先创建 if (patient == null) { patient = new Patient(); patient.setName(vo.getPatientName()); patient.setIdCard(vo.getIdCard()); patient.setPhone(vo.getPhone()); patient.setAddress(vo.getAddress()); patientMapper.insert(patient); } // 3. 生成挂号记录 Register register = new Register(); register.setRegisterNo(generateRegisterNo()); register.setPatientId(patient.getId()); register.setDeptId(vo.getDeptId()); register.setDoctorId(vo.getDoctorId()); register.setStatus("WAIT"); // 挂号费取自科室表 register.setFee(deptMapper.selectById(vo.getDeptId()).getRegFee()); register.setCreateTime(new Date()); registerMapper.insert(register); return Result.success(register); }generateRegisterNo()的生成规则是取当前日期加自增序号,比如R20250210003。如果诊所一天几千号人,这个规则可能不够用,但对于小型诊所完全够了。事务注解@Transactional是这套系统里最关键的保障,去掉它的话,第二个 insert 失败时会出现没有挂号记录的孤儿档案。
3.3 收费结算:金额计算必须用 BigDecimal
收费逻辑涉及处方明细表,每张处方对应多个药品条目。这套系统在计算总金额时用了BigDecimal,这一点值得所有初学者学习。如果用 double 计算药品金额,在涉及小数点后两位的场景里会出现0.1 + 0.2 != 0.3这类问题,导致对账永远对不上。
public Result charge(ChargeVO vo) { // 1. 查出该挂号单对应的所有处方明细 List<PrescriptionItem> items = prescriptionItemMapper .selectByRegisterId(vo.getRegisterId()); BigDecimal total = BigDecimal.ZERO; // 2. 逐条计算金额并累加 for (PrescriptionItem item : items) { Drug drug = drugMapper.selectById(item.getDrugId()); BigDecimal amount = drug.getUnitPrice() .multiply(BigDecimal.valueOf(item.getNum())); total = total.add(amount); } // 3. 记录收费流水 Charge charge = new Charge(); charge.setRegisterId(vo.getRegisterId()); charge.setTotalAmount(total); charge.setPayType(vo.getPayType()); // CASH / WECHAT / ALIPAY charge.setCreateTime(new Date()); chargeMapper.insert(charge); return Result.success(total); }注意这里没有做distinct去重,同一张处方里的同一种药可能出现多条记录,比如开了两盒阿莫西林,前端可能传了两条数量各为 1 的明细。这种情况下要正确累加,不能认为同一种药只会出现一次。收费完成后,挂号状态要同步更新为 FINISH,这个更新操作通常写在chargeMapper.insert之后,同一事务内完成。
3.4 药房发药:扣库存时的并发隐患
药房模块的代码是所有模块里最容易出问题的。发药时要扣减药品库存,看似只是执行一条 UPDATE 语句,但在多窗口同时发药的情况下,会出现典型的并发超卖现象。A 窗口和 B 窗口同时查到库存剩 10 盒,都各自扣了 5 盒,最后库存变成 5 而不是 0。
// Mapper 中的扣减语句 // 注意:先判断再扣减,避免超卖 @Update("UPDATE drug SET stock_num = stock_num - #{num} " + "WHERE id = #{drugId} AND stock_num >= #{num}") int deductStock(@Param("drugId") Long drugId, @Param("num") Integer num);这条 SQL 里把判断库存充足的条件直接写进 UPDATE 语句,数据库在执行时行锁会保证只有一条成功的更新。如果返回值等于 0,说明库存不足或药品不存在,此时 Service 层要抛出明确异常,前端弹窗提示「库存不足」。
我在另一个项目里见过反面教材:先SELECT stock_num判断,再执行UPDATE,中间隔了几行业务代码,结果并发一上来就翻车。这套 HIS 的处理方式虽然做不到像秒杀系统那样精细,但在诊所这种业务量级下已经足够安全。
4. 部署与运行:从空环境到跑通第一次挂号收费
4.1 环境准备要哪些东西
这套系统的运行环境要求不高,花半小时就能备齐。安装 JDK 8 并配好JAVA_HOME,安装 MySQL 5.7 或 8.0,再装一个 Maven 3.6 以上版本用于打包。前端部分不需要额外构建工具,因为静态资源已经编译好放在static目录里了,除非你想改前端页面结构。
我实际跑下来发现最容易被卡住的是 MySQL 版本差异。这套系统的初始化 SQL 在某些 MySQL 5.6 版本上执行会报错,建议直接用 MySQL 8.0,反而更稳。另外注意字符集要设置成utf8mb4,否则后面插入中文患者姓名时出现乱码,排查起来很浪费时间。
4.2 初始化数据库和执行 SQL
打开 MySQL 命令行或 Navicat,新建一个名为his_db的数据库,然后执行sql目录下的his_init.sql。这个脚本里包含建表语句和最初的几条测试数据:一个管理员账号、几个科室、三五种常用药品。
mysql -u root -p his_db < sql/his_init.sql执行完成后检查一下user表,默认账号通常是admin/admin123,密码存的是 MD5 密文。如果你用的是图形化工具,执行完脚本后刷新表列表,应该能看到前文提到的 patient、register、drug 等六张表。手动验证一下数据没问题再继续,免得后面排查问题时分不清是数据问题还是代码问题。
4.3 修改配置并启动后端
核心配置文件是src/main/resources/application.yml,数据库连接相关的配置都在这里改。重点确认url里的 IP、端口、数据库名,以及username和password是否正确。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/his_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver改完配置后在工程根目录执行 Maven 打包命令,跳过测试加快速度:
mvn clean package -DskipTests打包完成后,target目录下会生成一个his-simple-0.0.1-SNAPSHOT.jar,直接启动:
java -jar target/his-simple-0.0.1-SNAPSHOT.jar看到 Spring Boot 的启动日志出现Started HisApplication就说明成功了。打开浏览器访问http://localhost:8080,会自动跳转到登录页。
4.4 完整跑一遍流程验证功能
系统启动后,用admin/admin123登录。以门诊挂号的完整流程举例:先进入患者管理页新建一个患者档案,或直接去挂号页面输入身份证号查询,系统会自动弹出是否新建档案的提示。选定科室和医生后保存,会生成挂号单号和挂号费金额。然后进入收费页面选择该挂号单,添加药品明细,保存后完成收费。最后去药房发药页面找到该挂号单,点击发药按钮确认扣库存。
这四步全走通,说明系统在你这台机器上已经正常工作了。我建议你在这个过程中留意 console 里打印的 SQL 日志,看看 MyBatis 实际执行了哪些语句,这对后面定位问题很有帮助。如果某一步页面报错,优先去看后端日志的最后几行异常堆栈,八成是数据库连接或字段名对不上这类问题。
5. 避坑记录:这四个位置最容易翻车
5.1 首次启动直接连不上数据库
现象:后端启动时报Can't connect to MySQL server on localhost,启动直接失败。
原因:绝大多数情况是application.yml里的密码写错,或者是 MySQL 服务没启动。还有一个隐藏坑是 MySQL 8.0 默认加密方式跟驱动不兼容,旧驱动会有Public Key Retrieval is not allowed报错。
解决:先确认 MySQL 服务在运行,再在url末尾加上allowPublicKeyRetrieval=true,并把驱动版本升级到8.0.33以上。我在跑这套系统时遇到过两次,都是密码问题,血泪经验告诉我会先排查配置文件再动代码。
5.2 中文乱码看着像乱码实际是编码问题
现象:登录后患者姓名变成问号,或者数据库里的中文显示成???。
原因:数据库连接 URL 没加characterEncoding=utf8mb4,或者建库时没有指定字符集,默认用成了latin1。
解决:执行ALTER DATABASE his_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,同时把serverTimezone加上,顺手把表和已有数据的字符集一起改掉。从那以后凡是新建表,我都检查一遍默认字符集。
5.3 发药时库存明明是够的却提示不足
现象:药房发药时报库存不足,但查了一下 drug 表库存明明是正的,而且数量大于要发的数量。
原因:大概率是扣库存的 SQL 条件多写了一个AND status = 'ON_SALE',或者药品规格不匹配导致药品 ID 查不到。也可能是因为前端传的数量是字符串,后端用Integer接收时类型转换把值变成 0 了。
解决:先看后端日志里的 SQL 参数==> Parameters: 5(Integer), 3(Integer)这两个具体值,确认 ID 和数量都正常。再把 Mapper 的 SQL 精简到最小条件,重新测试。
5.4 修改前端资源后浏览器一直显示旧页面
现象:改了static目录里的 HTML 或 JS 文件,重新访问页面,效果没生效。
原因:Spring Boot 对静态资源做了缓存,浏览器本地也缓存了旧文件,两个缓存叠加导致看到的一直是旧页面。
解决:在项目里按 Ctrl+F5 强制刷新浏览器,同时在application.yml里加一段静态资源缓存控制,把默认关闭掉的缓存打开,方便开发时及时看到改动。
5.5 挂号单号重复导致主键冲突
现象:如下图所示,在高并发挂号时偶尔会遇到Duplicate entry 'R20250210001' for key 'register_no'。
原因:generateRegisterNo()用日期加自增序号生成单号,当天序号从数据库查询所得。如果两个请求同时查询到同一个最大序号,就会生成重复单号。
解决:给register_no列增加唯一索引兜底,再把单号生成改成「日期 + Redis INCR」或者直接用数据库的SELECT MAX(id)加锁。小诊所压力不大,加唯一索引后报错概率会显著降低,不会再遇到这种尴尬情况。
6. 二次开发:给 HIS 增加一个「挂号套餐」功能
6.1 业务背景与改造思路
诊所有一种常见业务:固定的体检套餐,包含三五种检查项目和对应药品,患者来直接买套餐,不用一项项单独开。现在的系统不支持一次绑定多项目,需要在挂号时选择一个套餐,自动生成处方明细并计算总价。改造思路核心是新建一张package表和一张package_item关联表,在用挂号接口里做分支判断。
6.2 数据库新增和代码改动
先建表:
CREATE TABLE `package` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL, `price` decimal(10,2) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `package_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `package_id` bigint NOT NULL, `drug_id` bigint NOT NULL, `num` int NOT NULL DEFAULT 1, PRIMARY KEY (`id`) );然后新增实体Package和PackageItem,Mapper 只需两个查询方法:按 ID 查套餐,查套餐下的所有项目。原有的RegisterService.createRegister里加一个参数packageId,当它不为空时,插入挂号单后额外循环查询package_item里的药品,自动生成多条处方明细。收费流程不用改,因为收费已经是按处方明细统计的,套餐的金额自然被包含进去了。
前端改动更简单:挂号页面加一个下拉框,数据从新增的/api/package/list接口获取。选中套餐后,让挂号费显示套餐价格,或者沿用原有挂号费叠加计费,看诊所的收费规则。
这套改造的妙处在于完全没有动原有的收费和药房逻辑,只新增了一张关联表和几个查询接口。验证方法是:建一个儿童体检套餐,包含血常规和维生素 D 检测,用新患者挂号选择该套餐,走完收费和发药流程,核对处方明细是否自动生成、总金额是否正确。我在自己搭的这个环境里走了一遍,从设计到验证大概三小时搞定。从那以后接外包项目,凡是和处方、库存相关的功能,我都会强制走一遍「建档 → 开单 → 收费 → 发药」的完整链路再交付,这套 HIS 系统让我养成了这个习惯。希望帮到你。
本文还有配套的精品资源,点击获取