我是怎么用Spring Boot搭医疗ERP的
我是怎么用Spring Boot搭医疗ERP的
前言
做医疗行业的程序员大概都有这个感受——市面上的ERP要么太重,要么太贵,要么根本不懂医疗行业的玩法。我之前在一家医疗器械公司做后台开发,天天看着业务人员用Excel管库存、用微信对账、用纸单子追批号,实在看不下去了,于是决定搞一个。
声明一下:这个项目并不是完全从零搭建的,而是基于市面上的开源项目改造而来。开源项目提供了基础的进销存框架,我在此基础上做了大量行业定制和功能扩展。站在巨人肩膀上比自己从零搭省事太多,这点得承认。
项目叫壶卢医疗ERP,名字取自"悬壶济世"的典故——葫芦是古代盛药的器具,也是中医的标志物,做医疗ERP,得有点仪式感。
项目地址:www.xnysj.cn:8088,支持自行注册使用。
技术选型
先说技术栈,不整花哨的,就是Spring Boot全家桶:
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 2.0.0 | 主框架 |
| JDK | 8 | 稳定为主 |
| MyBatis-Plus | 3.0.7 | ORM,多租户插件很好用 |
| MySQL | 5.7 | 数据存储 |
| Redis | - | 缓存+Token管理 |
| Netty | 4.1.x | WebSocket聊天服务 |
| Springfox | 2.7 | API文档 |
| EasyPOI | 4.4 | Excel导入导出 |
别急着吐槽版本老,后面会说到升级的事。
为什么选Spring Boot
说实话,选Spring Boot没什么特别的理由,就是熟。做Java后台的,Spring生态是绕不过去的,Spring Boot又是最省事的启动方式。对于一个人开发的中小型项目来说,自动配置、内嵌Tomcat、starter依赖管理,这些就是刚需。
另外一个重要原因是多租户——医疗行业客户多是小微企业,SaaS模式是刚需。Spring Boot + MyBatis-Plus的多租户插件,SQL层面自动拼tenant_id,业务代码完全不用管,这个组合非常省心。
核心架构
多租户SaaS
这是整个系统最核心的设计。每个客户是一个租户,数据完全隔离:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户插件,自动在SQL中拼接tenant_id条件
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从请求上下文获取当前租户ID
Long tenantId = TenantContext.getTenantId();
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 部分表不需要租户过滤
return skipTenantIdTables.contains(tableName);
}
});
interceptor.addInnerInterceptor(tenantInterceptor);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
这种方案的好处是:业务代码完全不需要关注租户隔离,MyBatis-Plus在SQL执行前自动拼条件,查询时加 WHERE tenant_id = ?,插入时自动填充tenant_id。缺点是数据库层是共享的,属于逻辑隔离,不是物理隔离。对医疗行业小微企业来说够用了。
模块划分
系统分三大块:
1. 公司ERP — 面向企业用户的核心业务
- 采购管理:请购单 → 采购订单 → 采购入库 → 采购退货
- 销售管理:销售订单 → 销售出库 → 销售退货
- 商品管理:商品信息、批号管理、库存修正
- 财务管理:收入单、支出单、收款单、付款单、转账单
- 报表查询:库存统计、采购销售统计、进销存、客户/供应商对账
核心业务流程:以销定购
这是整个系统最核心的设计,也是朋友公司日常用得最多的流程。
他们的业务场景是这样的:公司人不多,不是所有岗位都需要用系统,但采购是刚需——客户要什么,就采购什么,同一批采购可能要找不同供应商。所以系统的"以销定购"开关默认是开启的,整个流程走下来非常顺:
第一步:录销售订单
采购员先在"销售管理 → 销售订单"录一个单据,指定一个客户,商品明细里录入多条数据。这些商品可能来自不同供应商,但每个特定商品的供应商是相对固定的,所以供应商ID直接维护在商品信息里。选商品的时候可以根据供应商来筛选,非常方便。
第二步:销售订单转采购订单
这是关键操作。在销售订单页面有"转采购订单-以销定购"按钮,选择需要转的销售订单,点击后只需指定一个供应商,系统自动过滤出该供应商下的所有商品,一键生成一条采购订单。也就是说,一个销售订单可以自动拆分为多个采购订单,取决于销售订单里有多少个不同供应商的商品。
第三步:采购订单转采购入库
采购订单页面有"转采购入库"按钮,一键创建采购入库单据。入库时默认本次付款填0——因为货到了不代表钱付了。
第四步:付款闭环
财务登录系统后,在"财务管理 → 付款单"页面能看到所有有欠款的采购入库单,针对欠款单据创建付款单,形成采购和付款的闭环。
反方向同理:销售订单也可以通过"转销售出库"按钮快捷创建出库单据,出库后在"收款单"页面针对欠款创建收款单,形成销售和收款的闭环。
整个流程:销售订单 → 采购订单 → 采购入库 → 付款单,采购端闭环;销售订单 → 销售出库 → 收款单,销售端闭环。每一步都有快捷转单按钮,不用重复录入数据,效率很高。
2. 个人记账 — 这个是我自己加的,也是花心思最多的模块
很多人觉得ERP里放个人记账不伦不类,但实际使用中发现,很多小老板公私账目不分,ERP和个人财务混在一起管是真实需求。
随手记模块参考了市面上成熟的随手记App,在此基础上做了不少调整:
- 今日收入/今日支出:两个页签分别展示,分类清晰
- 分类管理:可以禁用用不到的分类,也可以修改分类别名,满足不同人的记账习惯
- 今日记事和备注:不只是记账,还能随手记事
- 图片附件:支持上传图片,拍照记账、存票据都很方便
- 大额收支:个人空间独有模块,可以在一个页面同时记录和预览收入与支出数据。虽然单条记录只能是收入或支出,但多条数据录入后列表页可以统一查看,一目了然
- 个人债务:借入借出管理
这里有个设计我觉得比较巧妙:系统分"公司ERP"和"个人空间"两种模式,它们是两个独立的租户,数据天然隔离。"账务管理"菜单下的"收入单"和"支出单"是两种模式都有的,但个人空间多了一个"大额收支"模块。在个人空间内,"大额收支"和"收入单/支出单"的数据是打通的——在"大额收支"里记的收入和支出,会同步出现在"收入单"和"支出单"列表中;反过来也一样。两个视角,一份数据,既能在"大额收支"里统览全貌,也能在"收入单"和"支出单"里分类管理。
说实话这块花了不少心思,尤其是分类管理——市面上的记账App分类都是写死的,我加了禁用和别名功能,灵活度一下子就上来了。用起来比专业的记账App还方便,因为数据在一个系统里,不用来回切。
3. 系统管理 — 权限和配置
- 租户管理员:角色管理、用户管理、机构管理、日志管理
- 超级管理员:功能管理、租户管理、商品属性、FAQ管理、插件管理
聊天工具
这个也是我自己加的,基于Netty实现的WebSocket聊天服务。说实话目前用户用得不多,没什么反馈,算是实验性功能。但功能还是做全了:
- 默认群组:租户内所有成员自动加入,消息全员可见
- 私聊:点对点即时沟通
- 系统助手:对接FAQ管理,用户提问自动匹配回答,相当于内置的智能客服
- AI助手:对接AI大模型,用户可以直接与AI对话,目前大模型部署在本地服务器,线上环境通过内网穿透访问
- 文件传输助手:跨设备传文件
- 好友系统:跨租户沟通,添加好友即可
系统助手是个比较有意思的设计。管理员在FAQ管理页面维护问答对(支持分类树、排序、启用/禁用),用户在聊天中向系统助手提问,系统自动匹配最相关的FAQ回答。这样常见问题不用人工回复,新员工也能快速上手。
医疗行业的特殊需求
做医疗ERP和做通用ERP最大的区别,就在批号管理上。
医疗器械有批号、生产日期、有效期,这三个字段对医疗行业来说非常重要。商品可以设置为无批号,也可以开启批号管理,是灵活可选的。目前用户还在熟悉系统阶段,暂时没有启用批号功能,等系统用顺了再把批号考虑进去,减少人工追批号的工作。
商品开启批号管理后:
- 采购入库时必须填写批号、生产日期、有效期
- 销售出库时必须选择具体批号
- 商品名称后面自动带"批"字标识
- 库存按批号维度统计,不是简单的数量汇总
这个看似简单的功能,对数据模型的影响是巨大的。同一商品不同批号是不同的库存记录,出入库要指定批号,报表要按批号维度展示。通用ERP不管这些,但医疗行业迟早要用到批号管理。
另外还有几个医疗行业的特色字段:
- 注册证号:医疗器械注册证编号
- 存储注意事项:冷链、避光等要求
- 生产厂家:与供应商可能不同,需要独立字段
这些字段在业务单据导出时会用到,不填就缺内容。
数据库设计要点
表结构
48张表,核心表几个:
jsh_depot_head/jsh_depot_item:单据主表/子表,所有出入库单据共用一套表,通过type字段区分jsh_account_head/jsh_account_item:财务主表/子表,同理jsh_material:商品表,40个字段,医疗行业字段多jsh_material_extend:商品价格扩展,支持多仓库多价格- 所有表都有
tenant_id和delete_flag,逻辑删除 + 租户隔离
单据编号
用 jsh_sequence 表实现自增编号,格式如 CGRK20260520001(采购入库+日期+序号),比数据库自增ID更适合业务展示。
遇过的坑
坑1:Spring Boot 2.0太老了
这是最大的坑。2.0.0版本对JDK 11+支持不完整,CGLIB反射访问在JDK 11会警告,JDK 17直接报错。升级计划8→11→17→21分四步走。
坑2:循环依赖
Spring Boot 2.6开始默认禁止循环依赖,我的项目里Service层有循环引用。解决办法:
- 简单查询的循环依赖:直接改调Mapper,去掉Service层的间接调用
- 有业务逻辑的循环依赖:加
@Lazy注解 - 第三方框架的循环依赖(springboot-plugin-framework、PageHelper):临时配置
spring.main.allow-circular-references=true兜底,后续升级框架版本根治
@Service
public class OrderService {
@Lazy
@Autowired
private UserService userService;
}
坑3:MyBatis-Plus大版本升级
从3.0.x升到3.5.x,整个插件体系重构了。PaginationInterceptor → MybatisPlusInterceptor,TenantSqlParser → TenantLineInnerInterceptor,SqlParserFilter → @InterceptorIgnore 注解。迁移工作量不小,但新API更清晰。
另外MyBatis-Plus 3.5.x带的JSqlParser 4.x对SQL解析更严格,ifnull(delete_flag,'0') !='1' 会报错,改成标准SQL写法 <> 即可:ifnull(delete_flag,'0') <> '1'。
坑4:sun.* 内部API
JDK 8可以随便用 sun.security.action.GetPropertyAction,JDK 11+默认不让访问。替换方案很简单:
// 旧写法
String tmpDir = AccessController.doPrivileged(new GetPropertyAction("java.io.tmpdir"));
// 新写法
String tmpDir = System.getProperty("java.io.tmpdir");
坑5:maven-assembly-plugin版本太低
2.5.1版本跟JDK 11不兼容,打包报 ZipArchiver 错误,升级到3.6.0解决。
坑6:Tomcat对URL特殊字符校验更严格
Spring Boot 2.7内嵌的Tomcat升级后,URL中的 {}、[] 等特殊字符会被拒绝,直接返回400。前端传JSON作为查询参数时必踩。解决办法:
server.tomcat.relaxed-query-chars={,},[,]
坑7:Springfox与Spring Boot 2.6+不兼容
Spring Boot 2.6默认路径匹配策略从 AntPathMatcher 改成了 PathPatternParser,Springfox不支持,启动直接NPE。临时解决:
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
后续Step 3会把Springfox换成SpringDoc,彻底解决。
坑8:空字符串转Long报错
Spring Boot 2.7对参数转换更严格了。前端传 ?id=(空值),旧版本空字符串转Long会得null,新版直接抛 MissingServletRequestParameterException。解决办法:要么逐个接口加 required = false,要么全局注册 CustomNumberEditor 允许空值。
坑9:MySQL驱动坐标变了
mysql:mysql-connector-java:8.0.33 已经迁移到 com.mysql:mysql-connector-j:8.0.33,Maven会警告。驱动类名也要从 com.mysql.jdbc.Driver 改成 com.mysql.cj.jdbc.Driver,连接URL需要加 serverTimezone=Asia/Shanghai 参数。
坑10:Redis客户端从Jedis换成了Lettuce
Spring Boot 2.x默认Redis客户端从Jedis切到了Lettuce,Lettuce的连接池依赖 commons-pool2,不加的话连接池功能不生效。
升级进度
JDK 8→21的升级计划,分四步:
| Step | 目标 | 状态 |
|---|---|---|
| Step 1 | JDK 8→11,编译运行通过 | ✅ 已完成 |
| Step 2 | Spring Boot 2.0→2.7 + 依赖升级 | ✅ 已完成 |
| Step 3 | JDK 11→17 + Spring Boot 2.7→3.x | 🔄 进行中 |
| Step 4 | JDK 17→21,最终验证 | ⬜ 待开始 |
升级分支名:upgrade/jdk21,每个Step打tag。
Step 2踩了最多的坑,一口气解决了循环依赖、MyBatis-Plus插件重构、Tomcat参数校验、Springfox兼容性、MySQL驱动迁移、Redis客户端切换等问题。Step 3是真正的坎——javax→jakarta命名空间迁移 + Spring Boot 3.x配置重写。
一些开发体会
1. 别过度设计
我的项目就是一个人开发的,没搞微服务、没搞消息队列、没搞分库分表。Spring Boot单体 + MySQL + Redis,够用了。过早优化是万恶之源,业务跑起来再说。
2. 多租户要早定
多租户方案(共享数据库逻辑隔离 vs 独立数据库物理隔离)是架构层面最早要做的决定。我选了共享数据库,用MyBatis-Plus的租户插件,开发成本最低。如果一开始没考虑多租户,后期加会非常痛苦。
3. 批号管理是医疗行业的刚需
做医疗行业ERP,批号管理迟早要用上。库存、出入库、报表、对账,所有环节都要考虑批号维度。开源项目自带了批号功能,可以先不启用,但数据模型设计阶段就要想清楚,后期加会非常痛苦。
4. 聊天工具目前是实验性功能
加聊天工具的时候觉得ERP系统内沟通成本高,采购问仓库、仓库问财务都跑到微信上去了。想法是好的,但实际上用户用得不多,目前算是实验性功能,后续看看有没有更好的切入点。
5. 开源项目是好的起点
壶卢医疗ERP是基于开源项目改造的,省了大量基础功能开发时间。开源项目本身已经包含了批号管理等功能,但通用方案和医疗行业之间还是有差距——医疗字段定制(注册证号、存储注意事项等)和聊天工具得自己搞。站在巨人肩膀上比自己从零搭省事太多。
写在最后
这个项目从立项到现在,断断续续做了两年多。一个人搞全栈,从后端到前端到部署,累是累了点,但看着系统从开源项目一步步改造到能在生产环境跑起来,还是挺有成就感的。
系统目前还在迭代,JDK升级进行中,后续还会加上更多功能。如果你也是做医疗行业的开发者,或者对Spring Boot ERP感兴趣,欢迎来体验:www.xnysj.cn:8088,自行注册即可使用。
有问题也可以在CSDN评论区交流,大家一起进步。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)