ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot医药管理系统实战:批号效期与库存预警设计

Spring Boot医药管理系统实战:批号效期与库存预警设计 这真的是毕设级医药管理系统而不是又一个药品进销存我这两年陆陆续续帮人看过的 Spring Boot 毕设项目里出现频率最高的就是XX管理系统而其中基于 Spring Boot 的医药管理系统几乎每隔几周就会遇到一次。选这个题目的同学通常想法很简单医药行业听着正规又不像电商那样功能复杂用 Spring Boot 做增删改查正合适。但真正动手之后才发现医药管理系统最坑的地方恰恰不是 CRUD而是它背后那一套行业规则——药品批号怎么管、效期怎么预警、特殊药品怎么控这些才是让这个项目从货架管理系统变成医药管理系统的分水岭。这篇文章我打算按一条完整的实战路径来拆从业务需求分析开始到技术选型、数据库建模、核心模块实现、权限安全再到部署上线和常见坑的排查思路。不管是拿它做毕业设计、课设还是想学 Spring Boot 完整实践你都可以直接照着落地。我会把每一步的为什么这样做也讲清楚而不是简单甩一段代码给你抄。先给没接触过的读者定个位医药管理系统本质上是进销存系统在医药行业的一个变体核心业务是采购、入库、销售、库存、效期管理和预警但比普通进销存多了两条硬约束——药品必须按批次追踪近效期药品必须提前拦截。这两条约束就是整个系统的复杂度来源。1. 开工前的业务梳理医药管理系统的核心需求到底是什么1.1 为什么说批号 效期才是系统的灵魂普通的超市进销存你只要管理好哪个商品、多少数量、什么价格就够了过期面包扔掉之后不用追溯是哪一批生产的。但药品完全不是这么回事。药品从出厂到患者手里每一盒都关联着生产批号、有效期、供应商、采购单号、销售去向一旦出现质量问题要能按批号把整条链路拉出来。这是医药管理系统区别于一切普通库存系统的根本点。所以在设计表结构之前你要先在脑子里建立一个概念药品主数据表只是存放药品的基础信息名称、规格、生产厂家、批准文号真正干活的是库存批次表。每一次采购入库根据采购单和批号生成一条库存批次记录每一次销售出库从批次记录里扣减数量。批号成为贯穿采购、入库、销售、预警、追溯的一条主线。1.2 第二个核心需求效期管理和近效期预警医药流通企业有个行话叫近效期一般指距离失效期还有 6 个月以内的药品。这些药不能等到过期那天再处理必须在近效期阶段就触发提醒——能退货的退给供应商能促销的做促销卖不动的下架报废。落到系统里这就是一个典型的定时任务场景每天扫描库存批次表计算失效日期 - 当前日期的天数分梯度生成预警记录。比如剩余天数预警级别建议动作0-30天严重立即下架停止销售31-90天较高重点催销联系退换货91-180天预警限制采购优先出库180 天是一个常见阈值你也可以按业务需要调整。这个功能做得好不好直接决定评审老师或者面试官对你的评价——很多人做完整个项目都没有一个成员函数是为预警写的那你做的就只是个普通进销存挂个医药的名头而已。1.3 特殊药品的管理边界除了批号和效期医药行业还有处方药和非处方药之分部分药品如含特殊成分的制剂在销售时需要登记购买者信息。毕设系统不需要做到医院药房那么严格但至少要预留两个字段药品类型处方药/非处方药和销售限制标识。前端展示和开单校验时我们可以对处方类药品加个确认弹窗或者限额控制表达我知道有这个业务约束的态度这在评审时是实打实的加分项。1.4 从需求反推功能模块清单梳理到这里功能模块自然就清晰了登录与用户管理系统管理员、采购员、库管员、销售员等角色药品信息管理药品 CRUD、分类维护、状态上下架供应商管理供应商信息、往来记录采购管理采购单创建、审核、入库确认库存管理批次台账、库存流水、库存盘点销售管理销售开单、批号选择、销售记录预警管理近效期预警、库存上下限预警统计报表入库统计、销售统计、库存结构分析这些模块做完才算是一个站得住脚的医药管理系统。2. 技术选型Spring Boot 项目从骨架到配套方案2.1 Spring Boot 版本与依赖选择既然标题就是基于 Spring Boot技术底座没什么悬念。但我强烈建议你用当前主流稳定版本而不是一上来就用最新版。原因很简单最新版本的同学会踩到很多依赖兼容性的坑而这些坑和你的业务一点关系都没有白白浪费时间。以 Spring Boot 2.7.x 为例搭配 Spring Framework 5.3.x生态非常成熟网上资料最多遇到问题一搜就有答案。如果你的环境是 JDK 82.7.x 是再合适不过的版本当然如果你已经习惯了 JDK 17可以往上选 3.x但要注意部分老教程里的配置类写法会有点差异比如WebSecurityConfigurerAdapter在 3.x 里已经废弃了需要改成基于SecurityFilterChain的 Bean 配置。我下文给的代码默认基于 2.7.x。下面是一个最基础、够用的pom.xml核心依赖清单parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web层 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- ORM框架MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- 权限控制 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2.2 ORM 框架选择我用 MyBatis-Plus 的三个理由现在 Java 后端做管理类系统MyBatis-Plus 基本是事实标准很少有人去写纯 MyBatis 的 XML 了。我的理由很实际第一单表 CRUD 不用写 SQLBaseMapper直接继承开箱即用第二分页插件一条配置搞定不用自己拼 LIMIT第三它提供的条件构造器LambdaQueryWrapper在写业务查询时非常顺手代码可读性好很多。比如查询某个供应商下的所有药品传统 MyBatis 你要写接口、写 XML、写 resultMap用 MyBatis-Plus 只需要ListMedMedicine list medMedicineMapper.selectList( new LambdaQueryWrapperMedMedicine() .eq(MedMedicine::getSupplierId, supplierId) .eq(MedMedicine::getStatus, 1) );当然多表关联查询依然要自己写 SQL但那种查询一般集中在报表模块数量有限可以接受。2.3 前端方案Thymeleaf 渲染还是前后端分离这个决策要看你做的是什么场景。如果目标是毕设答辩时间又紧用Spring Boot Thymeleaf Bootstrap是最快的路径不用跨域、不用联调、不用单独部署前端项目一个 jar 包全搞定。如果你是想练前后端分离那就用 Vue Element UI / Ant Design Vue后端只出 RESTful API这时候要把 CORS、Token 鉴权这些问题一起考虑进去。我的建议是除非你前端 Vue 已经很熟否则毕设项目用 Thymeleaf 更稳。前后端分离意味着你要同时维护两个项目部署环境也要折腾 Nginx工作量直接翻倍但对功能的帮助并不大。传统模版渲染方案完全足够展示你的 Spring Boot 能力。下面是个简单的 Thymeleaf 页面片段展示药品列表核心部分table classtable table-bordered thead tr th药品名称/thth规格/thth生产厂家/thth批号/thth有效期至/thth库存/th /tr /thead tbody tr th:eachstock : ${stockList} td th:text${stock.medicineName}/td td th:text${stock.specification}/td td th:text${stock.manufacturer}/td td th:text${stock.batchNo}/td td th:text${#temporals.format(stock.expireDate, yyyy-MM-dd)}/td td th:text${stock.stockQty}/td /tr /tbody /table3. 数据库设计医药系统的灵魂藏在表结构里3.1 核心表清单与设计思路数据库设计是这类系统最重要的部分评审老师和面试官都会盯着表结构提问。我直接把一套经过实际项目验证的表设计拆给你按重要性从上到下排。用户与权限三剑客5张表sys_user用户表sys_role角色表sys_user_role用户角色关联表sys_menu菜单/权限表sys_role_menu角色权限关联表这就是标准的 RBACRole-Based Access Control模型。灵活的地方在于以后想给某个角色加权限只需要往sys_role_menu插记录不用改代码。药品主数据与供应商2张表med_medicine药品表。字段包括medicine_name、specification、manufacturer、approval_no批准文号、medicine_type处方/非处方、unit、retail_price、supplier_id、statussup_supplier供应商表字段如supplier_name、contact_person、contact_phone、address库存核心3张表med_stock_batch库存批次表字段包括medicine_id、batch_no批号、purchase_id、purchase_price、sale_price、quantity、frozen_quantity、expire_date、statusmed_stock_flow库存流水表这是一个日志型表字段包括medicine_id、batch_no、flow_type入库/出库/报损/盘盈等、quantity、before_qty、after_qty、operator_id、create_timemed_stock_alert预警记录表也可以不落表实时计算但落表的好处是可以做已处理/未处理状态跟踪采购与销售4张表med_purchase_order采购单主表med_purchase_order_item采购单明细表med_sale_order销售单主表med_sale_order_item销售单明细表提示主表存单据头信息如单号、供应商/客户、总金额、状态、操作员、时间明细表存每一行买的什么药品、什么批号、多少数量、什么单价。这个头 行的结构是所有单据类业务的标准设计也是你面试时随手能拿出来的亮点。3.2 两张关键表的字段设计详解库存批次表是重点中的重点我给出完整建表 SQL 供参考CREATE TABLE med_stock_batch ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, medicine_id BIGINT NOT NULL COMMENT 药品ID, batch_no VARCHAR(64) NOT NULL COMMENT 生产批号, purchase_id BIGINT DEFAULT NULL COMMENT 采购单ID, purchase_price DECIMAL(10,2) NOT NULL COMMENT 采购价, sale_price DECIMAL(10,2) NOT NULL COMMENT 零售价, quantity INT NOT NULL DEFAULT 0 COMMENT 当前可用库存, frozen_quantity INT NOT NULL DEFAULT 0 COMMENT 冻结数量销售中锁定, expire_date DATE NOT NULL COMMENT 有效期至, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 2近效期 3过期 4停售, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_medicine_batch (medicine_id, batch_no), KEY idx_expire_date (expire_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存批次表;注意几个细节DECIMAL(10,2)而不是DOUBLE或者FLOAT——钱的计算不能有精度误差MySQL 里浮点类型是魔鬼。库存存放的是可用数量和冻结数量两个字段不是只放一个总库存。销售开单时先冻结库存取消订单时释放冻结确认收款后扣减可用并减少冻结。这套冻结 - 确认扣减的机制可以解决同用户多次点提交导致的超卖问题。批号和药品 ID 加联合索引因为后续所有查询都围绕这两个字段展开expire_date单独加索引因为每日预警任务要按这个字段扫描。库存流水表是业务透明度的保证设计上更像日志CREATE TABLE med_stock_flow ( id BIGINT NOT NULL AUTO_INCREMENT, medicine_id BIGINT NOT NULL, batch_no VARCHAR(64) NOT NULL, flow_type VARCHAR(20) NOT NULL COMMENT IN-采购入库 / OUT-销售出库 / RETURN-退货 / LOSS-报损 / CHECK-盘点, quantity INT NOT NULL COMMENT 变动数量, before_qty INT NOT NULL COMMENT 变动前可用库存, after_qty INT NOT NULL COMMENT 变动后可用库存, biz_no VARCHAR(64) DEFAULT NULL COMMENT 业务单号如采购单号、销售单号, operator_id BIGINT NOT NULL COMMENT 操作人, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_medicine (medicine_id, batch_no), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表;这个表能做到什么效果当你说某个批次这批药从入到出共走了多少数量、中间发生几次退货、谁操作的直接一条 SQL 查流水就能回答。这其实就是医药行业常说的可追溯在数据库层面的落地。没有这张表系统只是记录了结果没有记录过程追溯能力为零。3.3 数据库初始化数据的技巧项目交付时一定要配套一个init.sql初始化脚本里面至少包含建库语句、建表语句、内置管理员账号、基础角色和菜单数据。不然别人拿你的源码光是把环境跑起来就要折腾半天。管理员账号建议用 BCrypt 加密后的密码预置进去不要在代码里明文写密码。不知道怎么生成 BCrypt 密码的话Spring Security 里有现成的工具类随便写个测试类跑一下就能拿到。4. 核心业务模块的实现逻辑与关键代码4.1 用户登录与权限控制的落地方式Spring Security JWT 是目前最流行的组合。用户登录成功后后端生成一个包含用户 ID、用户名、角色集合的 JWT前端每次请求都带上这个 Token后端通过过滤器解析 Token 并放到SecurityContext里。角色和菜单权限可以用自定义注解 拦截器的方式判断。自定义一个权限注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); }在 Controller 方法上加注解RequiresPermission(stock:inbound) PostMapping(/api/stock/inbound) public Result inbound(RequestBody Valid StockInboundDTO dto) { // 入库操作 }再用一个 AOP 切面拦截判断当前用户是否拥有对应权限没有就返回 403。这种注解方式比在每个接口里手写if (user.getRole() ...)优雅得多也是业界常用的做法。如果不用 AOP也可以借助 Spring Security 的PreAuthorize(hasAuthority(stock:inbound))达到同样的效果看个人习惯。4.2 采购入库流程一单入、批次成、流水记一个完整的采购入库操作代码层面要处理三件事更新采购单状态、生成库存批次、写库存流水。这三步必须在一个事务里完成任何一个失败都要回滚否则就会出现单据已入库但库存没变的账实不符问题。核心 Service 代码示意Transactional(rollbackFor Exception.class) public void confirmPurchaseInbound(PurchaseInboundRequest req) { // 1. 校验单据状态必须处于待入库 PurchaseOrder order purchaseOrderMapper.selectById(req.getPurchaseOrderId()); if (order null || !order.getStatus().equals(PurchaseStatus.PENDING_INBOUND.getCode())) { throw new BusinessException(采购单不存在或状态不允许入库); } // 2. 逐条明细处理入库 for (PurchaseOrderItem item : req.getItems()) { // 2.1 查药品主数据 MedMedicine medicine medMedicineMapper.selectById(item.getMedicineId()); if (medicine null) { throw new BusinessException(药品不存在 item.getMedicineName()); } // 2.2 生成库存批次 MedStockBatch batch new MedStockBatch(); batch.setMedicineId(medicine.getId()); batch.setBatchNo(item.getBatchNo()); batch.setExpireDate(item.getExpireDate()); batch.setPurchasePrice(item.getPurchasePrice()); batch.setSalePrice(item.getSalePrice()); batch.setQuantity(item.getQuantity()); batch.setFrozenQuantity(0); batch.setStatus(BatchStatus.NORMAL.getCode()); medStockBatchMapper.insert(batch); // 2.3 写入库存流水 MedStockFlow flow new MedStockFlow(); flow.setMedicineId(medicine.getId()); flow.setBatchNo(item.getBatchNo()); flow.setFlowType(StockFlowType.IN.getCode()); flow.setQuantity(item.getQuantity()); flow.setBeforeQty(0); flow.setAfterQty(item.getQuantity()); flow.setBizNo(order.getOrderNo()); flow.setOperatorId(SecurityUtils.getCurrentUserId()); medStockFlowMapper.insert(flow); } // 3. 更新采购单状态为已入库 order.setStatus(PurchaseStatus.FINISHED.getCode()); purchaseOrderMapper.updateById(order); }注意一个问题同一批次再次采购时怎么办两个供货商可能给同一款药同一个批号这是医药行业非常常见的真实场景。如果按上面的逻辑就会插入两条 quantity 独立的批次记录。这其实是合理的——虽然批号相同但是入库时间是不同的、采购单是不同的、追溯链上它们是两个单独的来源。你在设计时不要想当然地把批号 药品做唯一索引否则第二批同批号的货就永远入不进去这会成为你项目里最尴尬的 Bug。4.3 销售出库与库存超卖的预防销售出库的逻辑和入库正好相反难点在于并发控制。很多人写扣库存时会这样操作StockBatch batch medStockBatchMapper.selectById(batchId); if (batch.getQuantity() purchaseQuantity) { batch.setQuantity(batch.getQuantity() - purchaseQuantity); medStockBatchMapper.updateById(batch); }这样写在高并发下一定会出问题。两个用户同时读到 quantity5都判断 53都去做扣减最终结果是 2 而不是 -1甚至可能是负数。解决方案有三种从轻到重方案一乐观锁推荐给你入门用在表里加version字段更新时带上版本号条件UPDATE med_stock_batch SET quantity quantity - #{qty}, version version 1 WHERE id #{batchId} AND version #{oldVersion}影响行数为 0 说明别人已经改过了需要重试。这个方案实现简单毕设里完全够用。方案二悲观锁用SELECT ... FOR UPDATE锁定行直到事务提交Select(SELECT * FROM med_stock_batch WHERE id #{id} FOR UPDATE) MedStockBatch selectForUpdate(Long id);这能百分之百防止超卖但会带来锁等待和并发性能下降适用范围比乐观锁窄一些。方案三Redis 分布式锁适用于多实例部署的场景单机部署没必要上这么重。实际我在项目里验证过单机 Spring Boot 应用里乐观锁 账户隔离一个用户同时只能有一个未完成订单已经足够保证库存正确性没必要引入 Redis 增加复杂度。4.4 近效期预警定时任务怎么设计前面业务分析提到了近效期预警这里给出实现方式。用 Spring 自带的Scheduled注解就可以不需要引入 Quartz除非你要搞复杂的表达式调度。Component Slf4j public class BatchExpireCheckTask { Resource private MedStockBatchMapper medStockBatchMapper; Resource private StockAlertService stockAlertService; /** * 每天凌晨执行一次 */ Scheduled(cron 0 0 2 * * ?) public void checkExpireBatches() { // 查询所有状态正常、且有效期在180天内的批次 LocalDate today LocalDate.now(); LocalDate deadline today.plusDays(180); ListMedStockBatch batches medStockBatchMapper.selectList( new LambdaQueryWrapperMedStockBatch() .eq(MedStockBatch::getStatus, BatchStatus.NORMAL.getCode()) .le(MedStockBatch::getExpireDate, deadline) .gt(MedStockBatch::getExpireDate, today) ); for (MedStockBatch batch : batches) { // 计算剩余天数决定预警级别 long days ChronoUnit.DAYS.between(today, batch.getExpireDate()); AlertLevel level days 30 ? AlertLevel.SEVERE : days 90 ? AlertLevel.HIGH : AlertLevel.WARNING; stockAlertService.createOrUpdateAlert(batch, level); } // 过期批次自动转停售状态 ListMedStockBatch expiredBatches medStockBatchMapper.selectList( new LambdaQueryWrapperMedStockBatch() .lt(MedStockBatch::getExpireDate, today) ); for (MedStockBatch batch : expiredBatches) { batch.setStatus(BatchStatus.EXPIRED.getCode()); medStockBatchMapper.updateById(batch); } log.info(近效期预警扫描完成预警批次[{}]过期批次[{}], batches.size(), expiredBatches.size()); } }别忘了在启动类上加上EnableScheduling别问我是怎么知道的。4.5 进销存报表的 SQL 写法除了日常业务报表也是这个项目的标配。月度进销存汇总报表本质上是围绕库存流水表做聚合。用一条 SQL 统计指定时间段内某药品的入库量、出库量、期末结存SELECT medicine_id, SUM(CASE WHEN flow_type IN THEN quantity ELSE 0 END) AS inbound_qty, SUM(CASE WHEN flow_type OUT THEN quantity ELSE 0 END) AS outbound_qty FROM med_stock_flow WHERE create_time BETWEEN #{startTime} AND #{endTime} GROUP BY medicine_id;期末结存用期初结存 本期入库 - 本期出库来算。这种基于流水的统计方式比直接查库存表靠谱因为库存表只反映当前瞬间的状态而流水保留了历史变化的全过程。5. 权限与安全医药系统的合规底线不能虚设5.1 RBAC 模型从数据库到接口的落地链路很多同学的权限设计停留在登录就能进或者一个字段判断身份的层面但一个像样的管理系统至少要分清四个角色管理员、采购员、库管员、销售员。管理员拥有全部权限采购员只管采购模块能新增采购单、审核库管员只管入库、盘点、库存查询销售员只管销售开单和销售记录。在数据库层面RBAC 的落地链路是用户登录获取用户 ID查sys_user_role得到角色集合查sys_role_menu得到菜单与权限标识集合把这些信息放入用户会话或 JWT 中每次请求由权限校验组件判断接口需要的权限标识是否在用户权限集合中我建议把权限标识用统一格式模块:操作比如purchase:create、stock:inbound、sale:create、system:user这样在配置菜单表时非常直观。5.2 密码存储与数据脱敏的底线做法密码存储必须用 BCrypt不要自己写 MD5 加盐。BCrypt 内置随机盐同一密码两次加密结果不同而且计算成本可以调高暴力破解难度大得多。用户注册和修改密码的 Service 里写法如下BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String rawPassword userDTO.getPassword(); String encoded encoder.encode(rawPassword); // 存储 encoded校验时用encoder.matches(rawPassword, encodedPassword)不要直接equals。接口返回数据时也要做脱敏处理比如用户的手机号显示成138****1234。这个需求用 Jackson 自定义注解 序列化器实现最优雅。声明一个Sensitive注解在字段上标注Data public class SysUserVO { private Long id; private String username; Sensitive(type SensitiveType.MOBILE) private String phone; }自定义序列化器里判断类型并做替换前端拿到的直接就是脱敏后的内容后端日志也不会把完整手机号打出去。这个细节如果写进你的项目说明里非常拉好感——说明你考虑过数据安全问题而 90% 的同类毕设都不会想这一层。5.3 接口防刷与操作审计管理类系统一般不会暴露在公网大流量访问下但接口层面的基础防护还是要有。至少两个方面登录接口限流同一个 IP 或用户名连续失败 5 次锁定 10 分钟。用ConcurrentHashMap做一个简易计数器就能实现不需要上 Redis。操作日志对关键的写操作采购入库、销售修改、用户删除等记录操作日志包括操作人、时间、请求参数、操作结果。可以单独建一张sys_oper_log表用 AOP 切面统一记录减少对业务代码的侵入。6. 部署与发布让项目能跑给别人看才是最后的完成6.1 多环境配置的正确写项目里建议放三份配置application.yml公共配置application-dev.yml本地开发环境数据库用本地 MySQLapplication-prod.yml生产环境数据库用服务器 MySQL并配置正式环境的数据源、日志级别启动命令区分环境java -jar medicine-system.jar --spring.profiles.activedev以常见的 2.7.x 项目为例生产环境配置大致长这样spring: datasource: url: jdbc:mysql://localhost:3306/medicine_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: xxxxxx driver-class-name: com.mysql.cj.jdbc.Driver server: port: 8080数据库连接串里的serverTimezoneAsia/Shanghai一定要加不然日期时间会相差 8 小时这是个极易踩的坑。6.2 打包、启动与进程守护在项目根目录执行mvn clean package -DskipTests之后在target目录下会生成medicine-system-0.0.1-SNAPSHOT.jar。在服务器上用nohup启动nohup java -jar medicine-system-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod app.log 21 看到日志输出Tomcat started on port(s): 8080基本就成功了。如果服务器配置不高可以在启动参数里加 JVM 调优参数限制内存占用比如nohup java -Xms256m -Xmx512m -jar medicine-system-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod app.log 21 云服务器还要在安全组或者防火墙里放行对应端口本地测试的话用curl http://localhost:8080/api/health验证接口通不通。6.3 将 Spring Boot 项目打成可执行 Jar 后常见问题部署之后最容易出问题的几个点端口被占用java -jar启动报Address already in use用netstat -tlnp | grep 8080查占用进程。MySQL 版本差异导致的时区问题报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized就是时区没写对的问题回到 6.1 的配置里加上serverTimezoneAsia/Shanghai即可。静态资源 404 或 JS/CSS 加载失败如果你用 Thymeleaf确认spring.thymeleaf.cachefalse只在开发环境开启生产环境保持true同时确认target/classes/static下确实编译生成了静态资源很多同学本地可以跑打 jar 后页面样式全丢就是因为静态资源路径配错或者模板路径缺了一层。Tomcat 启动缓慢小配置服务器上如果日志卡在Root WebApplicationContext: initialization in xxx ms注意看是不是 JVM 内存给得太小或者系统熵值不足导致 SecureRandom 阻塞可以在启动参数加-Djava.security.egdfile:/dev/./urandom缓解这也是老生常谈的坑。7. 开发中踩过的坑和复盘如果你也是新手这些比功能更值钱7.1 金额精度问题FLOAT 是魔鬼分单位存储是最稳的医药系统里每一笔采购、销售都涉及金额财务模块如果出现 0.01 的误差将来评审老师一句金额精度怎么保证就能把你问住。MySQL 中FLOAT和DOUBLE是近似存储不要用于金额。要么用DECIMAL(10,2)要么按分存储整形。国内系统习惯用元作为记账单位但实际电商行业主流做法是用分为单位因为整数运算永远不会有舍入问题。毕设不要求你实现财务总账但用DECIMAL是一个必须做对的底线。7.2 库存扣减的超卖问题一次并发场景让我从 0 分到及格我最初做库存扣减时就是查出库存、判断够不够、再更新单机测试从来没出问题。直到答辩前一天测试同学开了两个浏览器窗口、同一个账号同一件商品同时提交库存直接变成负数了。后来用了乐观锁版本号方案问题解决。为此我还写过一个测试用例启动 50 个线程同时抢购同一批次的 30 件药品断言最终库存不为负。这个小工具直接成了项目的加分材料。建议你也写一个类似的单元测试或者并发调试入口至少说明你意识到过这个坑。7.3 日期处理Date、LocalDate、LocalDateTime 别混用药品效期本质上只需要日期不需要时间采购单和销售单需要精确到秒的时间点数据库里这两种字段都存在。Java 层面建议统一使用java.time的LocalDate和LocalDateTime不要再碰java.util.Date。配合 MyBatis-Plus 的自动填充功能创建时间和更新时间可以统一处理TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;实现一个MetaObjectHandler就能在 insert/update 时自动填充不用每个 Service 里手动 set 当前时间。7.4 数据初始化脚本一定要给足我曾经拿到过一个开源项目代码写得不错表结构也全就是没有初始化脚本。我手动建了 20 张空表之后发现系统根本登录不进去——没有管理员账号。后来只能自己写 SQL 往用户表插记录又因为没有角色数据插进去也没有权限。后来我养成了习惯任何项目交付必须附上一个完整可执行的数据初始化 SQL。你的项目也应该在引言和 README 里写清楚恢复数据库的步骤。8. 个人实操体会这个项目做完之后我最大的收获是什么如果只能总结一句话我会说Spring Boot 只是工具业务理解才是这类管理系统的真正门槛。做这个项目之前我以为医药管理系统无非就是药品的增删改查做完之后才发现批号效期、近效期预警、库存流水、权限控制、金额精度每一个环节都在逼着你做超出CRUD 程序员的思考。你会开始问自己为什么要用DECIMAL而不是DOUBLE为什么要冻结而不是直接扣减库存为什么要用乐观锁而不是直接updateById。这些问题的答案恰恰是工作面试里最常被追问的内容。我也一直在跟选这个题目的同学说别把做完当成终点可以往这几个方向继续扩展给系统加上简单的采购建议功能根据库存下限和近效期状态自动生成补货清单做一张销售趋势看板用 ECharts 展示月度销售曲线和药品销售排行试试把预警消息推送到企业微信或钉钉机器人让近效期提醒不再依赖有人打开系统才看到如果精力还有富余可以用 Redis 做登录会话管理和分布式计数把不同角色的在线操作情况实时展示出来这些扩展点每做一个你的项目就比 80% 的同类毕设多一些差异化。最后再分享一个我个人的小习惯开发过程中每完成一个模块在 README 里更新一次模块进展并用截图记录当时的页面效果一朝一夕积累下来答辩材料根本不用临时赶。毕竟一个能讲清楚当时怎么设计、怎么踩坑、怎么解决的开发者才是评审老师真正想看到的。
返回列表