news 2026/10/14 22:58:23

SpringBoot药店管理系统实战:销售库存一体化设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot药店管理系统实战:销售库存一体化设计

1. 药店管理的真实痛点:为什么我需要一套销售与库存一体化的系统

做这个项目之前,我先后接触过两家规模不大的连锁药店,它们的经营方式非常有代表性:前台收银用一套简单的开单软件,后台库存靠库管员在 Excel 里手工维护,甚至还有一家直接用纸质台账。这种模式下最典型的问题有三个。

第一是账实不符。药店的销售高峰集中在上午和晚间,收银员一边扫码开单一边应付顾客咨询,经常出现漏扫、错扫的情况。到了月底盘点,系统里的库存数和货架上的实物对不上,少则几十元,多则上千元,谁也说不清问题出在哪个环节。第二是效期管理困难。药品是特殊商品,近效期药品如果不及时处理,就只能报废。但靠人工去翻台账排查近效期批次,工作量非常大,漏掉一两种是常有的事。第三是促销和会员业务没法做。药店经常有满减、折扣、会员积分等活动,纯手工或者用 Excel 去算,效率低不说,还容易产生客诉。

我做这个基于 SpringBoot 的智能药店管理平台,目标就是把这几个痛点一次解决:销售端和库存端共用一套数据,前台每完成一笔销售,库存实时扣减;后台可以按批次跟踪效期,设置近效期预警;再加上基于销售数据的统计报表,让店长能快速看清哪些药卖得好、哪些药滞销积压。整条链路用一套系统串起来,管店的人和写系统的人都能省不少心。

如果你是计算机相关专业的毕业生,正在为毕设选题发愁,或者已经在做类似的管理系统但总觉得模块之间比较散,这篇文章可以作为一份相对完整的参考。我会把从项目结构设计、数据库建模到核心业务实现的完整思路都过一遍,最后还会聊聊我实测中踩过的坑,以及答辩时评审老师通常会关注哪几个点。

2. 技术选型与项目骨架:SpringBoot 生态下如何搭出可答辩的架构

2.1 为什么主框架选 SpringBoot 而不是 SSM

现在高校的 Java 课程大多还在讲 SSM(Spring + SpringMVC + MyBatis),但到了做毕设的阶段,我建议直接上 SpringBoot。原因很实际:SpringBoot 把大部分配置都自动完成了,你不需要再写一堆 XML 去声明 Bean,也不用为视图解析器、事务管理器这些基础组件费神。对于一个人从零开始写一个管理系统的毕设场景,SpringBoot 能帮你把精力集中在业务逻辑上,而不是耗在框架配置的调试里。

另外,SpringBoot 内置的 Tomcat 使得项目可以直接打成 Jar 包运行,这对后期部署演示非常友好。答辩演示的时候,现场启动一个应用比在 IDE 里折腾部署流程要稳妥得多。

我最终选定的技术栈如下表所示,这套组合在"开发效率、学习成本、答辩可讲性"三者之间比较均衡:

层次选型选择理由
后端框架SpringBoot 2.7.x社区资料多,兼容性好,稳定
持久层Spring Data JPA实体映射简洁,建表方便,适合快速开发
数据库MySQL 5.7 / 8.0主流、免费、资料多
权限认证Spring Security + JWT状态无侵性,前端分离时方便
前端Vue 2 + Element UI组件丰富,管理后台开发效率高
接口文档Swagger / knife4j调试方便,答辩展示加分
构建工具Maven主流标配,无争议

补充一点:如果你是刚接触 Spring Data JPA 的新手,也可以换回 MyBatis,因为很多学校的课程都教过 MyBatis,熟悉度更高。但 JPA 在这个项目里有一个很实用的好处——实体类定义好后可以自动建表,省去手写大量建表 SQL 的功夫,效率提升明显。两种方案都能做完整套系统,关键在于你自己对哪个更熟。

2.2 项目分层:包结构如何组织才能让老师一眼看懂

毕设项目最怕的是代码堆在一起,Controller 里写 SQL、Service 里写前端逻辑的情况一旦出现,即便功能都跑通了,答辩时也很容易被问倒。我采用的包结构是一个很标准的四层划分,既有清晰的纵向分层,也有按业务模块进行的横向组织:

com.pharmacy.platform ├── controller // 接口层:接收请求、参数校验、返回统一响应 ├── service // 业务层:事务管理、核心业务逻辑 │ └── impl // 业务实现类 ├── repository // 数据访问层:继承 JpaRepository 的接口 ├── entity // 实体类:与数据库表一一对应 ├── dto // 数据传输对象:VO(视图对象)、QueryParam(查询参数) ├── config // 配置类:安全配置、跨域配置、Swagger 配置 ├── common // 通用类:统一返回结果、异常处理、工具类 └── security // 认证授权相关:JWT 过滤器、登录处理器

这里想特别提醒一个细节:entity 和 dto 一定要分开。很多初学者图省事,直接把实体类返回给前端。这在简单项目里可能没什么,但一旦你需要做"查询药品列表时不返回库存成本价"或者"创建订单时需要传一个前端用不到的字段",就会非常别扭。分开之后,Controller 层只操作 DTO,Repository 层只返回 Entity,中间用转换器衔接,代码会清爽很多,后面加需求也容易。

2.3 前后端分离还是服务端渲染?我的建议

现在的毕业设计,除非导师明确要求用 JSP,否则我强烈建议做前后端分离。前后端分离展示出来的"工程化"程度更高,答辩时也可以讲"前端 Vue 负责渲染、后端 SpringBoot 负责接口、通过 RESTful API 交互",这句描述本身就是个加分项。

但要注意,前后端分离不等于把工作量翻倍。你可以把前端页面做得适度精简,核心放在"药品管理、库存管理、销售开单、统计报表"这几个页面上,其他页面保持简洁风格即可。另一个建议是:如果你时间紧张,可以先用服务端渲染跑通全部功能,再抽时间做前端页面;我的实际经验是,大部分学校的毕设评审更看重后端业务逻辑的完整性,前端达到"能用、不难看"就足够了。

我实际开发中采用的方案是:后端接口用统一响应体返回,前端通过 Axios 调用。统一响应体的结构很简单:

@Data public class Result<T> { private Integer code; // 200 成功,500 失败,401 未登录 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

只要所有 Controller 都返回这个结构,前端处理逻辑就非常统一,拦截器里判断 code 即可,不用每个接口单独处理异常分支。

3. 药品进销存核心流程的数据库设计:五张表如何撑起整个系统

3.1 核心表结构拆解

药店的业务流程看起来复杂,但抽象到数据层面,其实就是"药品信息、库存批次、采购入库、销售出库、往来客户"这五个核心概念。我最终设计的表结构围绕这几张表展开,这里挑最关键的几张说明设计思路。

药品信息表 drug

字段类型说明
idbigint主键
drug_codevarchar(32)药品编码,唯一索引
drug_namevarchar(64)药品通用名
specificationvarchar(64)规格,如 0.25g*24片
manufacturervarchar(128)生产厂家
approval_numbervarchar(64)批准文号
unitvarchar(16)基本单位,盒/瓶/袋
retail_pricedecimal(10,2)零售价
purchase_pricedecimal(10,2)进货价
categoryvarchar(32)分类:处方药/非处方药/中药饮片/保健品
prescription_requiredtinyint是否处方药
statustinyint0下架 1在售 2停用

库存批次表 stock_batch

药品最忌讳混合批次管理。同样一种药,三个月前进的货和三天前进的货,批号、效期完全不同,如果只用一个库存总数,就没办法做效期预警和先进先出。所以我把库存拆成了批次表:

字段类型说明
idbigint主键
drug_idbigint关联药品表
batch_novarchar(64)生产批号
production_datedate生产日期
expiry_datedate有效期至
quantityint当前库存数量
supplier_idbigint供应商
warehouse_locationvarchar(32)货位号,如 A-03-02

每次采购入库时,系统按"药品 + 批号"为依据创建或累加批次记录。这个设计是整套系统的地基,没有它,后续的效期预警和先进先出都是空中楼阁。

采购入库单表 purchase_order 与明细表 purchase_order_item

主表记录入库单号、供应商、入库总金额、入库时间、操作员;明细表记录每种药品入库数量、进货单价、批号、效期。一张入库单对应多条明细,标准的一对多关系。这里要强调:入库单一旦审核通过,必须自动同步更新 stock_batch 表对应的批次数量。这个联动如果写不好,就会出现"入库单显示进了 100 盒,但库存没变"的尴尬情况,后面排查起来非常痛苦。

销售订单表 sale_order 与明细表 sale_order_item

销售主表记录订单号、收银员、会员手机号(可空)、订单总金额、实付金额、优惠金额、下单时间。明细表记录售出的药品、数量、单价、小计。销售的核心难点在于事务性:扣减库存和生成订单必须要么都成功、要么都失败,绝不能出现"订单生成了但库存没扣"的情况。这个用 Spring 的@Transactional注解就能保证。

3.2 为什么要把"库存变更"写成流水账

除了上面几张核心表,我还加了一张库存流水表 stock_log。不要小看这张表,它在实际项目里价值极高。

@Entity public class StockLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long drugId; private Long batchId; private Integer changeQuantity; // 正数入库,负数出库 private Integer afterQuantity; // 变更后库存 private String changeType; // PURCHASE_IN 采购入库 / SALE_OUT 销售出库 / STOCK_ADJUST 盘点调整 private String remark; private LocalDateTime createTime; private Long operatorId; }

每次库存变动都往这张表写一条记录,相当于给库存装了行车记录仪。后面如果出现"某天库存对不上"的问题,直接查流水表就能定位是哪笔操作导致的。而且答辩时老师问"你怎么实现库存追溯",你把这张表的设计逻辑讲清楚,是一个很扎实的加分点。

3.3 JPA 实体关系的处理细节

用 JPA 设计这几张表的关联关系时,我最大的体会有两点。

第一,多对多关系尽量拆成一对多。比如"药品—供应商"是多对多,但直接建多对多表会丢失额外信息(比如这个供应商是这种药的主供应商还是备选供应商)。我选择单独建一张drug_supplier_rel关联表,额外附带供应商类型字段,这样扩展性更好。

第二,避免在实体里写过于复杂的对象导航,比如SaleOrder里持有List<SaleOrderItem>是自然的,但不要再从SaleOrderItem反向持有Drug完整对象。查询时如果需要药品名称,可以用一个简单的 VO 查询(比如自定义 JPQL 查询返回需要的字段),也可以在 Service 层手动组装。这样做的好处是避免 JPA 的 N+1 查询问题,答辩时如果你能主动说出"我做了查询优化,避免了一次循环查数据库",老师会明显感觉到你不是只会 CRUD。

4. 权限设计与多角色工作流:收银员、库管员、店长看到的不是同一个系统

4.1 三种角色的职责边界

药店管理系统和普通的学生社团管理系统有一个明显区别——它的角色分工非常明确,而且每个角色能做的事情边界很清晰。我最终确定了三种角色:

  • 收银员(CASHIER):只能使用收银开单、退药、查看自己当班的销售记录。
  • 库管员(STOCK_MANAGER):负责采购入库、盘点、库存调整、效期管理,但不能查看营业额和毛利数据。
  • 店长(ADMIN):拥有全部权限,包括查看销售统计报表、管理员工账号、设置药品上下架。

这种设计不是拍脑袋想出来的,而是基于实际药店的经营规则——管钱的和管货的要分开。收银员如果既能改库存又能改订单,就有监守自盗的空间。这个业务逻辑讲到答辩老师那里,显得你不仅会写代码,还懂行业规则,这是毕设拿高分的一个非常重要的软实力。

4.2 Spring Security + JWT 的落地配置

我采用 Spring Security + JWT 做认证授权。JWT 的亮点在于无状态——服务端不需要保存 Session,前端把 Token 存下来,每次请求放在 Header 里即可。相关依赖和基本流程是这样:

首先,定义一个 JWT 工具类,负责生成和解析 Token:

@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; // 过期时间,单位毫秒 public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJwt(token) .getBody(); } }

然后写一个过滤器,在每次请求进入 Controller 前校验 Token:

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Claims claims = jwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}"); return; } } filterChain.doFilter(request, response); } }

最后在 SecurityConfig 里配置放行规则。这里有一个实战细节:登录接口、Swagger 文档路径、静态资源必须放行,其余接口全部需要认证;接口访问控制建议通过"请求路径前缀 + 角色"的方式配置,比如/api/stock/**只允许 STOCK_MANAGER 和 ADMIN 访问,/api/sale/**三种角色都能访问但收银员只能操作自己的订单。另外我在方法级别还用了一个自定义注解@RequireRole("ADMIN"),配合 AOP 做二次校验,确保即使 URL 被绕过,方法内部的权限校验也能兜住底。这种纵深防御的思路说出来,答辩老师会觉得你的系统设计是经过思考的。

4.3 一个容易忽略的权限漏洞:越权访问

权限设计上我踩过一个深刻的坑,分享出来提醒大家。当时我写完收银员端"查看自己的销售记录"这个接口后,直接用了saleOrderRepository.findByCashierId(cashierId)来查询。功能本身没问题,但有一个严重漏洞:如果收银员 A 把 URL 里的订单 ID 改为收银员 B 的订单 ID,就能看到别人的订单数据,这就是典型的水平越权。

修复方式很简单:查询时不仅校验角色,还要校验资源归属。例如查询订单详情时,必须同时校验"订单的收银员 ID 是否等于当前登录用户 ID"(管理员除外)。我建议在 Service 层抽一个通用的权限校验方法,例如:

private void checkOrderOwner(SaleOrder order, Long currentUserId, String currentRole) { if (!"ADMIN".equals(currentRole) && !order.getCashierId().equals(currentUserId)) { throw new BusinessException("无权查看他人订单"); } }

这个问题几乎是评审老师必问的考点,主动在项目文档里提一句"我做了资源级权限校验,防止水平越权",会是一个非常亮眼的加分回答。

5. 核心业务代码落地:销售扣库存与效期预警的实现细节

5.1 销售开单的完整事务链路

销售开单是整个系统里事务性最强的操作。我最终实现的流程是:前端提交订单主信息 + 药品明细列表,后端在一个事务里完成五件事——校验药品状态、计算价格、扣减库存批次、生成订单主记录、生成订单明细记录。核心方法如下:

@Transactional(rollbackFor = Exception.class) public SaleOrderVO createOrder(SaleOrderRequest request, Long cashierId) { // 1. 非空校验 if (request.getItems() == null || request.getItems().isEmpty()) { throw new BusinessException("订单明细不能为空"); } // 2. 计算总金额,校验库存 BigDecimal totalAmount = BigDecimal.ZERO; List<SaleOrderItem> orderItems = new ArrayList<>(); for (SaleOrderItemRequest item : request.getItems()) { Drug drug = drugRepository.findById(item.getDrugId()) .orElseThrow(() -> new BusinessException("药品不存在")); if (drug.getStatus() != 1) { throw new BusinessException("药品【" + drug.getDrugName() + "】已下架"); } // 扣减库存:按效期最近的批次先出(FEFO) int remain = item.getQuantity(); List<StockBatch> batches = stockBatchRepository .findAvailableBatchesByDrugIdOrderByExpiryDateAsc(item.getDrugId()); for (StockBatch batch : batches) { if (remain <= 0) break; if (batch.getQuantity() >= remain) { batch.setQuantity(batch.getQuantity() - remain); saveStockLog(batch, -remain, "SALE_OUT", "销售出库"); remain = 0; } else { remain -= batch.getQuantity(); saveStockLog(batch, -batch.getQuantity(), "SALE_OUT", "销售出库"); batch.setQuantity(0); } stockBatchRepository.save(batch); } if (remain > 0) { throw new BusinessException("药品【" + drug.getDrugName() + "】库存不足"); } BigDecimal subtotal = drug.getRetailPrice() .multiply(BigDecimal.valueOf(item.getQuantity())); totalAmount = totalAmount.add(subtotal); // 组装订单明细 } // 3. 生成订单 SaleOrder order = new SaleOrder(); order.setOrderNo(generateOrderNo()); order.setCashierId(cashierId); order.setTotalAmount(totalAmount); // ... 其他字段 // 4. 保存订单与明细 return convertToVO(saleOrderRepository.save(order)); }

这里有两个关键逻辑必须展开说明。

第一个是出库顺序。药店行业的规范做法是"近效期先出"(FEFO,First Expiry First Out),而不是"先进先出"(FIFO)。因为药品是特殊商品,离失效期越近的批次风险越大,必须优先卖掉。代码里findAvailableBatchesByDrugIdOrderByExpiryDateAsc这个查询就是按有效期升序排列批次,优先扣减最早到期的。这个细节如果你能在答辩时讲出来,老师会觉得你真的了解业务,而不是只会写增删改查。

第二个是库存扣减失败后的回滚。如果循环扣减到一半,发现某一种药库存不足,整个事务必须全部回滚。@Transactional(rollbackFor = Exception.class)保证了这一点。但注意,rollbackFor一定要写成Exception.class而不是默认的RuntimeException.class,否则如果你在业务代码里抛的是一个自定义的受检异常,事务不会回滚,后果就是部分扣库存成功、部分失败,账目直接乱掉。这个坑我实际遇到过,调试了一下午才定位到问题。

5.2 效期预警:定时任务加手动检查双保险

效期管理是这个系统的灵魂功能之一。我设计了一个"双通道"的预警机制。

第一个通道是定时任务,每天凌晨扫描一次近效期药品:

@Component public class ExpiryCheckTask { @Autowired private StockBatchRepository stockBatchRepository; @Autowired private NotificationService notificationService; @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void checkNearExpiryBatches() { LocalDate today = LocalDate.now(); LocalDate warnDate = today.plusMonths(3); // 3个月内到期算预警 List<StockBatch> warnBatches = stockBatchRepository .findByExpiryDateBetweenAndQuantityGreaterThan(today, warnDate, 0); for (StockBatch batch : warnBatches) { notificationService.createWarning( batch.getDrugId(), "药品【" + batch.getDrug().getDrugName() + "】批号【" + batch.getBatchNo() + "】" + "将于" + batch.getExpiryDate() + "到期,剩余" + batch.getQuantity() + "盒" ); } } }

第二个通道是手动刷新。在库存列表页面提供一个"效期检查"按钮,库管员随时可以手动触发检查,不必等定时任务。我加这个按钮的原因是实际使用中发现:定时任务虽然每天跑一次,但库管员不可能每天登录系统看通知,往往是两周后才登录一次,此时有些药品的效期已经非常紧张了。手动触发让库管员在每次盘点时都能主动掌握效期状况,两家药店的库管员对这个功能的反馈都很好。

5.3 销售统计报表:让店长一眼看懂经营状况

统计报表模块我用的是"按时间维度 + 按药品维度"两个视角。时间维度提供日报、周报、月报的销售曲线;药品维度提供单品销售排行、滞销药品排行。这些接口最核心的 SQL 逻辑可以简化成日期分组聚合:

public interface SaleOrderRepository extends JpaRepository<SaleOrder, Long> { @Query("SELECT new map(DATE(s.createTime) as date, SUM(s.totalAmount) as amount, COUNT(s) as orderCount) " + "FROM SaleOrder s " + "WHERE s.createTime BETWEEN :start AND :end " + "GROUP BY DATE(s.createTime) " + "ORDER BY date") List<Map<String, Object>> getDailySummary(LocalDateTime start, LocalDateTime end); }

有一点要提醒:报表数据不要每次都实时全量聚合。当订单表数据量大了之后,一个月的数据几万条,每次都全量聚合,接口响应会非常慢。我当时采用了"统计汇总表 + 定时刷新"的方案,每天晚上把前一天的销售数据汇总到一张daily_sale_summary表,前端报表页面默认读汇总表,只有需要看当天实时数据时才走实时查询。这样报表接口的响应时间基本能控制在 200 毫秒以内。这个优化点也可以用在实际项目中,如果你在论文里写"通过预聚合方案降低报表查询延迟",又是一个加分项。

6. 盘点与近效期管理的实操经验:库存对不上的时候怎么办

6.1 盘点流程的设计:一张盘点单怎么走完整个闭环

药店每年至少要进行两次全面盘点,加上不定期抽查,盘点功能如果做得不好用,库管员会非常抗拒使用系统。我设计的盘点流程是这样的:库管员发起盘点单 → 按货位顺序录入实盘数量 → 系统自动对比账面数量 → 生成差异明细 → 库管员确认差异原因(盘盈/盘亏/破损/过期)→ 审核通过后自动调整库存并写入库存流水。

这个流程里有几个非常关键的细节。第一,盘点单创建时系统会冻结账面数量快照。如果盘点过程中销售还在进行,账面数一直变化,就没办法对比差异。我在创建盘点单时把当前账面数存到盘点明细表里,之后以这个快照为准。第二,盘亏调整必须写明原因。我加了一个必填字段"调整原因",不允许空着提交,这样经手的每一笔库存调整都有据可查。第三,盘点差异超过一定金额时需要店长二次审核。这个规则是实际业务需要——如果盘亏金额过大,可能是管理问题,不能由库管员一个人关单。

6.2 过期药品处理:从预警到报损的完整链路

近效期药品如果在预警期内没有卖完,就会变成过期药。过期药不能直接扔掉,需要走报损流程。我在系统里把这个流程设计为:禁售 → 报损 → 出库 → 生成报损记录。当一个批次的药品过期后,系统自动把相关药品状态改为"禁售",前台收银无法再销售该药品,防止误售过期药——这是药店经营的合规底线。然后库管员操作"报损出库",把过期批次数量清零,系统自动生成报损单和库存流水记录(变更类型为DAMAGE_OUT)。这样财务可以依据报损记录做成本核算。

这个功能我建议所有做药店系统的同学一定要写进项目里。它把"效期预警、禁售控制、报损出库"三个环节串成了完整的闭环,远比单独做一个"增删改查药品"要有说服力。

6.3 盘点时最常出现的三类差异及排查方法

我在真实使用中总结了三类高频差异,也给同学们留作排查参考:

差异现象可能原因排查方法
账面数比实物多销售出库时扣错批次查库存流水表,核对 SALE_OUT 记录
账面数比实物少入库单审核后未同步批次表,或采购验收时数量录入有误检查入库单明细与实际到货数量
某个批次数量异常收银员销售时选择了错误的批次出库检查销售明细与批次关联关系

排查的思路万变不离其宗:任何时候库存对不上,先查库存流水表。如果流水也对不上,那就是最早的数据源头有问题。所以再次强调流水表的重要性,它在排查问题上能节省数小时时间。

7. 前端页面的关键交互:收银台、库存看板与权限菜单

7.1 收银台页面:速度优先的交互设计

药店收银台页面和普通列表页不同,它的核心诉求是快。我在设计收银页面时做了几个针对性优化:药品搜索支持"扫码、药品名关键字、拼音首字母"三种方式;加入购物车后自动计算总价;点击结算时弹出优惠选择面板(满减、折扣、会员价);结算完成后自动清空购物车并进入下一单。

其中让我花了最多心思的是拼音首字母搜索。药店顾客有时候说不出药品的准确商品名,只记得大概的发音。我在药品表里增加了pinyin_code字段,存的是通用名的拼音首字母,比如"阿莫西林胶囊"存为AMXLJN。搜索时直接模糊匹配这个字段,对于高频药品特别实用。这个功能虽然简单,但实际使用率极高,收银员对这个小细节的评价非常高。

7.2 库存看板:让库管员一屏掌握全局

库存页面的核心是"批号维度的表格展示 + 多条件的组合筛选"。我实现的表格列包含:药品编码、药品名、批号、规格、生产日期、有效期、剩余数量、货位号、剩余天数(自动计算)。同时支持按"效期剩余时间区间""药品分类""库存数量低于某个阈值"进行组合过滤。前端用 Element UI 的 el-table 加 el-tag,把近效期批次用红色标签标注、库存不足用黄色标签标注,视觉效果非常直观,库管员一打开页面就能看出今天最紧急的事是什么。

这里想分享一个前端小细节:所有状态枚举在前端做成统一字典,不要在每个页面写死。比如药品状态(在售/下架/停用)在药品管理页、收银台、库存页都会用到,写死的话后期改一个状态就要改三四处的代码。我在前端建了一个dict.js文件,统一导出所有枚举,页面通过dictMap['DRUG_STATUS'][status]这样取文案。这个习惯能让你少吃很多改需求的苦头。

7.3 动态菜单:不同角色登录后看到不同的导航

这个功能实现起来并不复杂,但很多新手容易忽略。前端路由完全按角色过滤,后端在登录接口里返回当前用户的角色信息和可访问的菜单列表,前端根据这个列表动态生成侧边栏。比如库管员登录后看不到"员工管理"菜单,收银员登录后看不到"采购入库"菜单。

实现上我用的是 Vue Router 的动态路由注册,登录后拿到菜单数据再router.addRoutes。这个做法的好处有两个:一是从界面层面杜绝了低权限用户操作高权限功能的可能,二是让"多角色系统"这个特征在答辩演示时非常直观——你可以现场切换三个账号,展示三种不同的界面,这个演示效果远超静态截图。

8. 我实测中遇到的坑与答辩前的准备清单

8.1 三个让我印象深刻的 Bug 复盘

第一个是JPA 懒加载导致的序列化异常。实体类里的关联对象用了@ManyToOne(fetch = FetchType.LAZY),在 Controller 层直接把实体返回给前端时,Jackson 序列化触发懒加载,结果抛出LazyInitializationException。当时困扰了我一整天。解决方案有两种:一是在 Service 层把实体转换为 DTO 后再返回(推荐,从根上规避);二是在配置里关闭懒加载(不推荐,影响性能)。我后来彻底采用第一种方案,再也没出过这个错。

第二个是金额计算丢精度。第一次写订单金额时用的double类型,结果遇到类似 3.6 元乘以 3 盒这种计算,出现 10.800000000000001 这种结果。财务数据出现这种误差是非常尴尬的。后面我把所有金额字段全改成BigDecimal,并且在实体定义和数据库字段都统一用decimal(10,2),问题彻底解决。所有涉及钱的 Java 项目,都应该在第一天就用 BigDecimal。

第三个是定时任务在没有加锁的情况下重复执行。我用的 Spring Task 在单机部署时没什么问题,但如果你把它部署到多实例环境(比如答辩前想在两台机器上跑演示),定时任务会在每个实例上都执行一遍,导致预警通知重复发送。我在任务方法上加了一个基于 Redis 的分布式锁,确保同一时刻只有一个实例执行任务。这个方案对毕设来说略微超纲,但如果你能主动说出来,会显得你考虑了生产环境的稳定性。

8.2 测试数据准备与答辩演示脚本

答辩前一周,我建议你花一天时间准备一套真实感极强的演示数据,这比临场造数据效果好一百倍。具体包括:

  • 录入 50 种以上的药品,覆盖处方药、OTC、保健品、中药饮片,至少 5 种近效期药品;
  • 造一批真实的供应商、会员信息;
  • 导入过去三个月的销售记录(可以用脚本随机生成,但要注意销售时间和药品匹配,不要出现已下架药品还有销售记录);
  • 准备两三个典型的盘点异常场景。

演示脚本我建议按"角色走查"来设计:先以店长账号登录,展示药品管理、员工管理、统计报表;然后切换库管员账号,演示采购入库、盘点、效期预警;最后切换收银员账号,演示最快的开单流程。每个角色选两到三个核心操作即可,不要面面俱到,关键是节奏紧凑。整个演示控制在 8 到 10 分钟比较合适。

8.3 论文与答辩中容易被追问的三个技术点

评审老师大概率会从三个方向追问,提前准备好回答思路很重要。第一个是事务边界:"销售开单过程中库存扣减失败了怎么办?"你要能回答出@Transactional的回滚机制,以及为什么要设置rollbackFor = Exception.class。第二个是权限体系:"如果一个收银员绕过前端直接调接口删订单怎么办?"你要能回答出后端接口层面的角色校验和资源归属校验。第三个是并发问题:"两个收银员同时卖同一盒药,库存会不会变成负数?"你要能说出乐观锁(@Version字段)或数据库行锁的方案,我这里用的是批次表更新时加WHERE quantity >= ?的条件更新,这种思路老师听了也会认可。

我个人的建议是:这些问题不要等老师问了才想,写论文的时候就要在"系统设计"章节里主动写清楚。你在文档里把这些问题作为"关键问题与解决方案"列出,答辩时会主动很多,也显得你思考透彻。

最后一个实用技巧:把项目的 README 写详细一点,包括技术栈、运行步骤、默认账号、测试数据说明。答辩时很多老师会现场要求你运行系统,有了清晰的 README,你五分钟就能把环境跑起来,而其他同学可能在配置数据库上卡十分钟。这个细节虽然不起眼,但实际影响很大。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 22:58:09

LunarLander强化学习实战:DQN改进与训练避坑指南

简介&#xff1a;面向强化学习初学者与TensorFlow使用者&#xff0c;基于LunarLander登月器环境的强化学习案例提供了一套完整可运行的Python工程&#xff0c;解决从环境搭建、模型训练到效果验证的实践痛点。资源共6个文件&#xff0c;核心为3个Python脚本&#xff0c;分别承担…

作者头像 李华
网站建设 2026/10/14 22:57:58

数据导入检查实战:从样例准备到比对脚本的完整指南

我自己平时刷软件测试相关的公众号&#xff0c;2026年的内容风向和早几年差别挺大的。以前大家爱看“某工具安装教程”“某框架入门”&#xff0c;现在打开公众号文章列表&#xff0c;标题里高频出现的关键词变成了“数据质量”“AI辅助测试”“接口自动化”“导入导出”&#…

作者头像 李华
网站建设 2026/10/14 22:57:16

昇腾AI算力实战:从大模型训练到推理部署的守正与开放

1. 大模型淘金热背后的算力焦虑1.1 为什么所有人都在谈"淘金"这两年跟同行聊天&#xff0c;话题绕来绕去最后总会落到同一个点上&#xff1a;算力。不管你是做NLP的、搞CV的&#xff0c;还是原来跟AI八竿子打不着的传统行业开发者&#xff0c;只要沾上大模型&#xf…

作者头像 李华
网站建设 2026/10/14 22:57:05

数据库检索结果差异大?别误判,这样应对才高效

在写毕业论文、准备开题报告或进行文献综述时&#xff0c;很多研究者都会经历一种令人困惑的情况&#xff1a;同样的关键词&#xff0c;在不同数据库中的检索结果差异非常大。 例如&#xff0c;你可能在某个数据库中检索到几十篇相关论文&#xff0c;但在另一个数据库中却只看…

作者头像 李华
网站建设 2026/10/14 22:56:03

Unity跑酷游戏源码解析:从环境配置到二次开发实战

简介&#xff1a;一份国外风格的Unity仿神庙逃亡完整项目源码&#xff0c;定位给Unity学习者和跑酷类游戏开发者&#xff0c;用来研究经典跑酷玩法、二次开发或直接改造成自己的作品。压缩包体积947.89MB&#xff0c;包含12584个文件&#xff0c;覆盖866个prefab预制体、488个C…

作者头像 李华
网站建设 2026/10/14 22:55:25

原生JS组态软件实战:无后台监控界面的数据格式与架构设计

简介&#xff1a;一份基于原生JavaScript实现的组态软件源码包&#xff0c;面向Web前端与工业自动化开发者&#xff0c;用于在无后台依赖环境中通过拖拽方式快速构建电力一次接线图、工业流程图等图形界面。压缩包共1013个文件&#xff0c;约14.4MB&#xff0c;以svg、png、gif…

作者头像 李华