"医药公司进销存管理系统"是计算机毕业设计里生命力最强的一类题目。前阵子又有同学拿着"基于SpringBoot的东明镇医药公司库存与销售管理平台"来问怎么下手,翻一下近几年的选题清单,"东明镇医药企业供应链信息化系统"这类换皮题目也一直没断过。标题怎么换都行,核心技术其实是一套:SpringBoot做后端,数据库管数据,前端做交互,把采购、销售、库存、报表这些进销存业务完整跑通。它的核心价值在于把药品这种有严格效期要求的货物当成管理对象,借助SpringBoot生态把前后端、数据库、报表链路串起来。这篇文章我就从毕设选题和实际开发两个角度,把这个系统从需求拆解到落地部署完整讲一遍。如果你正打算做这个题目,或者想快速做一套能拿去答辩的进销存系统,这篇可以当实操参考。
1. 核心需求拆解:进销存系统到底在解决什么问题
1.1 医药行业的特殊业务规则不能忽略
大多数人一想到进销存,第一反应就是增删改查,但医药公司的进销存和普通超市、服装店的进销存差得很远。药品不是普通商品,它有批号、有效期、生产日期、批准文号等硬性属性,还要符合GSP管理要求。最典型的功能是近效期预警,比如系统要在药品距离有效期还有90天的时候自动标红提醒,让仓管员优先出库这一批。再比如同一款药品,不同批次的入库价格可能不同,出库时一般要按先进先出扣减。如果你在设计表的时候连"有效期""批次号"这两个字段都没有,答辩时老师问"你这个医药系统和卖衣服的商城系统有什么区别",场面会很尴尬。
所以在第一版需求里,就要把药品主数据、批次库存、供应商资质这些医药行业概念放进系统。药品主档要包含通用名、商品名、规格、生产厂家、批准文号、剂型、单位、采购价、零售价、储存条件是不是冷藏等。库存不能只存一个总数量,必须按批次存多行,每一行对应一个生产批号和有效期。这才是医药领域进销存区别于普通库存系统的本质。
1.2 从毕设评估角度拆解题目含金量
这类题目为什么一直热门,因为它覆盖了软件工程基础课里的所有环节。"进"对应采购订单和入库管理,"销"对应销售订单和出库管理,"存"对应实时库存、盘点和预警,"管理"对应药品档案、供应商、客户和系统用户权限。从技术栈看,SpringBoot是当前大多数高校Java课程的主线,用它做后端既是主流选择,又容易查资料、找人讨论。从工作量看,这是一个中等偏上的规模,适合一个人独立完成,也方便在论文和答辩中讲出完整故事:你从需求调研、数据库设计、接口开发做到前端联调,整个软件生命周期都走了一遍。
老师给这类题目的评分点也相对清晰:数据库关系设计是否合理,业务逻辑是否有闭环,界面是否完整,答辩演示是否流畅。比如你做了销售出库,但没做销售退货,这就等于业务闭环断了,会被追问;你做了演示,但没提前准备测试数据,现场搜索商品时表格是空的,又会被扣印象分。把这些问题都提前规避掉,这个题目拿高分并不难。
1.3 三条业务主线先画清楚
写代码之前,我建议先把业务流程梳理成三条线。
第一条采购线:仓库缺货后发起采购申请,选供应商,生成采购单,到货后验收入库,库存增加,同时形成应付记录。第二条销售线:开销售单,选客户,出库扣减库存,形成应收记录;如果客户退货,再做销售退货单把库存加回去。第三条库存线:日常出入库流水、报损报溢、库存盘点、近效期预警、低库存提醒。
无论页面菜单和数据库表分成多少层,最终都是围绕这三条主线展开。你可以在论文的概要设计章节里用文字和功能模块图把这三条线画出来,这是评委最容易认可的"系统分析"证据。代码写在哪个包下面,表的主外键怎么关联,也都跟着这三条线走,不会乱。
2. 技术选型:一版兼顾答辩和企业实践的方案
2.1 后端框架:SpringBoot锁版本比追新重要
后端我明确建议用SpringBoot 2.7.x,比如2.7.18,不要一上来就追3.x新版本。原因是网上大多数资料、教学视频、现成demo都基于2.x,你遇到报错去搜索,答案基本都能直接用到。3.x要求JDK17起步,部分国产中间件和MyBatis-Plus老版本的兼容性还可能出问题,毕业设计阶段没必要在这上面浪费时间。
ORM层用MyBatis-Plus,理由很直接:它自带分页插件、代码生成器、Lambda条件构造器,能让你的开发效率翻倍。传统SSM当然也能写完这个系统,但各种XML配置、Spring整合配置非常繁琐,真正花到业务逻辑上的时间反而变少。如果你担心答辩时被问底层原理,把"MyBatis-Plus只是MyBatis的增强工具,底层仍然是MyBatis的Mapper代理和SQL映射"这个回答记住就够了。
构建工具用Maven,这也是毕业设计最通用的方案。对应一句话:Java Maven项目构建方法SpringBoot,就是通过mvn clean package把项目打包成可执行Jar,再java -jar启动。这句话实操层面就够用了。
2.2 前端路线:Thymeleaf还是Vue,取决于你的时间
前端有两条典型路线。
如果你预算只有两三周,选Thymeleaf + Bootstrap。Thymeleaf是服务端渲染,后端Controller返回ModelAndView,页面里直接写th:each遍历数据。它的好处是省掉跨域、Token、接口联调这些前后端分离的额外成本,一个人写完就能跑,非常适合时间紧的同学。
如果你想让系统完整度更高,或者题目里明确写了"SpringBoot+Vue",那一定要选前后端分离。前端用Vue 2或Vue 3配合Element UI,后端提供JSON接口。这里我多提醒一句:很多教程讲的是Vue打包之后放进SpringBoot的static目录,你只要在vue.config.js里把publicPath改成./,outputDir指定到SpringBoot项目的src/main/resources/static,构建后就能打成单个Jar运行。这样演示时不需要单独启动前端工程,一个命令全搞定。
2.3 数据库设计:批次库存是医药系统的命门
核心表我用一张清单列出来,你建表时按这个方向走就错不了:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 系统用户 | username、password、real_name、role_id |
| sys_role | 角色 | role_name、role_code |
| drug_info | 药品档案 | drug_name、specification、manufacturer、approval_number、purchase_price、sale_price |
| stock | 批次库存 | drug_id、batch_no、production_date、expiry_date、stock_num、warning_days |
| supplier | 供应商 | supplier_name、contact、phone、qualification_end_date |
| customer | 客户 | customer_name、contact、phone、license_code |
| purchase_order | 采购单主表 | order_no、supplier_id、total_amount、status、create_time |
| purchase_order_item | 采购单明细 | purchase_order_id、drug_id、quantity、price |
| sales_order | 销售单主表 | order_no、customer_id、total_amount、status、create_time |
| sales_order_item | 销售单明细 | sales_order_id、drug_id、stock_id、quantity、price |
| stock_log | 库存流水 | stock_id、change_type、change_num、before_num、after_num、create_time |
| expiration_warning | 效期预警 | stock_id、warning_type、handle_status、create_time |
最核心的设计要点是:stock表以药品加批次为唯一行,而不是每个药品只存一行。这样才能做到不同批次的入库价格分开、有效期分开、先进先出走清楚。其他文学化的设计,比如单独建一个库存汇总表来做页面展示,也可以,但底层必须保留批次库存明细,否则预警功能做不真实。
3. 实操核心:从零搭一套能演示的医药进销存
3.1 项目初始化与基础配置
这一步我用实操顺序讲。
先创建一个普通Maven项目,pom.xml引入SpringBoot 2.7.18父依赖,再加starter-web、MyBatis-Plus、MySQL驱动、Lombok。核心依赖片段大概是这样的:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> </dependencies>application.yml里配置数据源时,URL一定要加上useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai,否则很容器出现中文乱码和时区报错:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/medicine_inventory?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto建表时建议在建库语句里直接用CREATE DATABASE medicine_inventory DEFAULT CHARACTER SET utf8mb4;,避免后面插入药品名称里的特殊字符时出现问题。
3.2 登录认证与角色权限落地
登录功能可以有两套实现方案。
如果前端是Thymeleaf,用Session + 拦截器就够了。用户登录成功后把用户对象放进Session,写一个LoginInterceptor拦截未登录请求,页面菜单按角色判断是否显示。这套方案简单、容易讲清楚,也方便在答辩时现场演示"未登录访问会跳回登录页"。
如果前端是Vue分离模式,推荐用JWT + 拦截器。登录接口校验用户名密码后,用jjwt生成一个带用户信息和过期时间的Token返回给前端,前端每次请求带上Authorization头,后端拦截器解析Token,解析失败返回401。密码存储一定要用BCryptPasswordEncoder加密,不要明文落库,这一点在答辩时经常被问到。
权限建议做三到四个角色就够了:管理员、采购员、销售员、仓库管理员。管理员有全部权限,采购员只能访问采购单和供应商,销售员只能开销售单和管客户,仓管员负责入库、出库、盘点和预警。后端做接口级校验,前端做菜单级隐藏,两层一起控制。
3.3 采购入库与销售出库的业务实现
采购入库的关键是:生成采购单主表后,遍历明细,把药品按批次写入stock表并增加库存流水。同一个药品如果已有相同批号,就累加数量;没有就插入新批次行。这里我习惯在stock表上建唯一索引(drug_id, batch_no),防止代码并发时重复插入批次。
销售出库则是整套系统的难点,也是答辩最容易被追问的地方。我给出一个标准写法。
先查销售单明细,逐条扣减批次库存,扣减SQL必须带着库存充足条件:
@Transactional(rollbackFor = Exception.class) public void salesOutStock(Long salesOrderId) { List<SalesOrderItem> items = salesOrderItemMapper.selectList( new LambdaQueryWrapper<SalesOrderItem>() .eq(SalesOrderItem::getSalesOrderId, salesOrderId) ); for (SalesOrderItem item : items) { int rows = stockMapper.deductStock(item.getStockId(), item.getQuantity()); if (rows == 0) { throw new RuntimeException("药品库存不足或批次已被其他订单占用,请刷新后重试"); } } }对应的Mapper SQL:
<update id="deductStock"> UPDATE stock SET stock_num = stock_num - #{quantity}, update_time = NOW() WHERE id = #{stockId} AND stock_num >= #{quantity} </update>这段逻辑里最关键的是WHERE stock_num >= #{quantity},它保证了并发情况下不会把库存扣成负数。如果两个订单同时扣同一个批次,数据库行锁会让第二个更新等待,等第一个提交后检查条件不满足,更新行数为0,于是抛异常回滚。这就是最简单的乐观锁扣减方案。
销售退货是反方向操作,在批次库存上做数量累加,同时生成一条负向流水。报损报溢单也类似,只是不需要关联客户。把这三块都做完,业务闭环基本就完整了。
3.4 库存预警和效期提醒
近效期预警是这个医药系统的亮点功能,实现方式不复杂但效果很加分。
启动类加@EnableScheduling,写一个定时任务每天凌晨扫描批次库存,找出有效期在预警天数范围内且还没过期的批次,写入预警表。核心代码:
@Component public class ExpirationTask { @Scheduled(cron = "0 0 1 * * ?") public void checkExpiration() { List<Stock> warningList = stockMapper.selectList( new LambdaQueryWrapper<Stock>() .le(Stock::getExpiryDate, DateUtils.addDays(new Date(), 90)) .ge(Stock::getExpiryDate, new Date()) ); for (Stock stock : warningList) { // 写入 expiration_warning 表,标记未处理 // 业务上还可以设置预警类型:三个月预警、一个月预警、已过期 } } }页面上再做三个提醒角标:近效期药品数量、过期药品数量、低库存药品数量。哪怕你不发短信不发邮件,光在首页用红色数字展示这几项,演示时老师就会觉得系统有业务深度。如果时间更充裕,还可以给供应商资质到期做一个提醒表,算是医药供应链的特色功能。
3.5 报表统计的可视化
毕设演示时,图表是最直观的亮点。推荐用ECharts把首页做成驾驶舱风格:本月销售趋势、采购入库趋势、库存TOP10、近效期药品统计。
后端给四个统计接口就好,比如按月统计销售金额:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(total_amount) AS total_amount FROM sales_order WHERE status = 2 GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month;前端用一个通用的axios.get('/dashboard/salesTrend')拉到数据,配置成ECharts折线图。注意DATE_FORMAT按月份分组时,如果数据跨年,建议把月份字段改成%Y-%m格式,否则2023年1月和2024年1月会挤在一起。
报表不需要做得很复杂,但一定要保证按时段筛选有效,比如提供"近7天、近30天、近12个月"三个选项,演示时点一下切换,交互感立刻就有了。
4. 常见问题和毕设答辩避坑实录
4.1 并发扣库存导致超卖
这是一个非常典型的问题。很多同学用MyBatis-Plus的updateById更新库存,先查出来判断数量够不够,再更新,看起来没问题,但并发下两个请求同时读到同一库存量,后提交的更新会把库存覆盖成负数。解决办法就是我上面写的条件更新SQL,把判断和扣减放在一条UPDATE里完成。实现事务时,记得@Transactional要加在Service实现类方法上,rollbackFor = Exception.class也要写全,否则Spring默认只在遇到RuntimeException时才回滚,CheckedException不会触发回滚。
4.2 SpringBoot版本太高、依赖冲突怎么处理
"SpringBoot版本太高"这个热词确实扎心,很多同学直接创建SpringBoot 3.2项目,然后发现JDK版本不对、MyBatis-Plus版本不对、MySQL驱动坐标找半天。我的建议是新建项目时一定选择Spring Initializr里的2.7.x版本,或者直接改pom.xml父依赖版本。依赖冲突最常出现在分页插件上,如果你既引入了pagehelper-spring-boot-starter,又配置了MyBatis-Plus的分页插件,运行时就会报PageMethod相关错误。选一个体系用到底,别混用。
Maven打包时如果遇到"SpringBoot打包后Jar没有主清单属性",检查是否在pom.xml里引入了spring-boot-maven-plugin,并且mvn package是在项目根目录执行的。
4.3 答辩高频问题准备
把下面这几个问题提前准备好,答辩会顺利很多:
"为什么用MyBatis-Plus?"回答:它是MyBatis的增强工具,保留MyBatis原生能力的同时提供通用Mapper、分页插件和代码生成器,适合中小型管理系统快速开发。
"为什么库存要按批次存?"回答:药品有有效期属性,不同批次价格和有效期不同,只有按批次管理才能实现先进先出和近效期预警,这是医药行业GSP合规的要求。
"药品过期了系统怎么处理?"回答:定时任务扫描后会把过期批次状态改为过期,禁止销售出库,进入报损流程,同时生成报损单减少库存。
"你的事务怎么控制的?"回答:在Service方法上使用@Transactional声明式事务,扣库存操作放在同一事务内,任何一步失败都会整体回滚,保证库存和订单数据一致。
"页面权限和接口权限分别怎么实现?"回答:页面菜单根据角色代码动态渲染,后端接口通过拦截器校验登录状态和权限注解,两层配合。
把这些问题用你自己的话翻译一遍,尽量在答辩演示时主动讲出其中的一到两个,比如出库时故意演示库存不足的提示,反而显得项目有真实感。
4.4 常见问题速查表
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 部署后中文乱码 | 数据库表字符集不是utf8mb4 | 建库建表统一使用utf8mb4 |
| 扣库存超卖 | 判断数量和更新不是原子操作 | 改成UPDATE ... WHERE stock_num >= #{quantity} |
| 页面报404 | Vue打包路径不对,静态资源没进jar | 调整publicPath为./并重新打包 |
| ECharts图表不显示 | 统计接口返回字段和前端不一致 | 统一字段名为totalAmount、month |
| 定时任务不执行 | 启动类没加@EnableScheduling | 启动类补上注解 |
| 接口抛SQL语法错误 | Mapper XML用了多个数据库方言 | 核对MySQL版本和分页插件配置 |
5. 五周时间规划与毕设提效技巧
5.1 一份可以照抄的时间表
如果你想用五周把这套系统做完,我建议这样分配:
第一周做需求分析和数据库设计。哪怕不写代码,也要把三张业务主线图、数据库ER图、页面菜单结构全部画出来。这一周的东西就是论文里的核心素材,也是后面开发的地图。第二周搭环境和做登录权限。SpringBoot项目建好,Vue工程初始化,登录和路由跑通,你就有信心继续了。第三周做采购入库和销售出库,这是最核心的业务,把事务、库存流水、状态管理写扎实。第四周做预警、报表和页面美化,补齐近效期提醒和ECharts统计。第五周专门做测试和演示准备,写论文、做PPT,录一遍完整演示流程,排查漏洞。
5.2 标题怎么变、核心怎么改
"东明镇医药公司进销存管理系统""东明镇医药公司库存与销售管理平台""东明镇医药企业供应链信息化系统"这些标题,本质上是同一套系统的不同包装。如果你觉得东明镇这个名字有地域限制,可以换成你家乡的镇名或者"某医药公司",数据表不需要变。真正要改的是论文里的系统背景、用例图里的角色名称、系统名称的措辞。把这些内容做成可配置的占位符,最终交付时统一替换,效率会高很多。
我个人在指导毕设时最深的体会是:这个题目做完不难,做好要花心思。代码多不等于分高,业务闭环完整、演示流畅、关键逻辑能讲清楚才是拿分重点。所以最后分享一个实用小技巧:准备一批真实的测试数据,比如30个药品、10个供应商、10个客户、3个月的采购销售记录,所有页面筛选、搜索、图表都有内容可展示;演示前把数据库重新初始化一遍,保证在答辩现场不会因为脏数据翻车。这套系统的后续扩展空间也很大,加一个Redis做缓存热点药品、加一个Activiti做采购审批流、用SpringBoot整合Flink做门店销售实时统计,都是可以在论文创新点上延伸的方向。但作为毕业设计,先把眼前的进销存核心链路做得稳稳当当,比什么都重要。