Spring Boot药店管理系统,这类题目在课程设计和毕业设计里出现频率极高,几乎所有Java方向的学生都绕不开“XX管理系统”这个经典命题。但说实话,大部分同学做出来的东西只是把增删改查套了一层壳,数据库几张表、页面几个表格,答辩能跑通就完事。而“药店管理系统”这个场景,恰恰是管理系统里少有的、具备真实业务复杂度的题目——它涉及库存批次、药品有效期、销售主从表、多条件检索、权限分级这些不只是“增删改查”的内容。这篇文章,我就围绕一个可落地的药店管理系统,从业务拆解、技术选型、数据库设计、核心实现到部署避坑,完整讲一遍我是怎么做的,希望能给准备做类似项目的同学一些参考。
需要说明的是,文中涉及的代码和结构,基于Spring Boot + MyBatis-Plus + MySQL这套主流组合,这也是目前该题目最常用、也最方便后续扩展的技术路线。无论你是照着做还是在此基础上魔改,这篇文章的核心思路都能复用。
1. 药店管理系统到底在管什么:从业务场景到功能清单
1.1 药店的日常运转需要哪些业务闭环
先别急着写代码,做这个项目的第一步是搞清楚药店真实的业务流转是什么样的。很多同学一上来就建表、写增删改查,结果做完一看,药品能新增能删除,但“库存怎么扣减”“过期药品怎么预警”“一笔销售怎么对应多张明细”这些问题全都没想清楚。
一家小型药店的日常业务,核心可以拆成两条链:
- 采购入库链:药店向供应商进货,每个药品批次进入门店库存,对应的是入库单,可能一个入库单包含多种药品。
- 销售出库链:顾客来买药,收银台生成一笔销售单,一笔销售单包含多行药品明细,每一行明细都要扣减对应库存。
这两条链之外,还有基础数据和辅助功能:药品信息维护、药品分类、供应商信息、员工账号、客户或者说会员信息、以及财务相关的统计报表。
如果项目里只有一张“药品表”,那严格意义上不叫药店管理系统,那叫药品信息登记表。真正的难点和加分项,恰恰在于采购和销售这两条业务链背后牵扯出的主从表结构和库存流转逻辑。
1.2 功能清单:哪些功能是核心,哪些是加分项
我的做法是先划分角色,再为每个角色定功能。药店系统最常见的角色有三种:系统管理员、店长(经理)、收银员/普通员工。权限分级是这个项目区别于普通CRUD的重要标志,也是最容易在答辩时被老师追问的点。
核心功能模块我整理如下表:
| 模块 | 核心功能 | 优先级 | 说明 |
|---|---|---|---|
| 用户登录与权限 | 登录、退出、会话校验、角色权限拦截 | 必做 | 可用Spring Security,也可以自己写拦截器 |
| 药品管理 | 药品新增、编辑、上下架、详情 | 必做 | 关联药品分类、厂家(供应商) |
| 药品分类 | 分类维护、树形或平级分类 | 必做 | 用于前台检索与统计 |
| 供应商管理 | 供应商增删改查、联系人信息 | 必做 | 简化版可以并入采购模块 |
| 采购入库 | 入库单创建、入库历史 | 必做 | 入库即增加库存批次 |
| 库存管理 | 当前库存查询、库存预警、批次详情 | 必做 | 预警是药店系统的特色功能 |
| 销售管理 | 收银开单、销售历史、销售明细 | 必做 | 一笔销售单对应多行明细 |
| 退货管理 | 销售退货、采购退货 | 选做 | 有精力就加,属于加分项 |
| 统计报表 | 近7日销售趋势、热销药品Top10、利润统计 | 加分项 | 用ECharts画图展示 |
| 药品过期预警 | 按有效期筛选即将过期或已过期批次 | 加分项 | 药店场景的核心特色 |
功能清单出来之后,项目边界就明确了:一个基础版要覆盖的,是用户权限 + 药品/分类/供应商基础维护 + 采购入库 + 库存管理 + 销售开单这五个部分。超过这个范围的功能,比如会员储值、医保对接、扫码枪硬件对接,属于真商业项目才需要考虑的方向,课程设计阶段不建议一上来就铺开做。
2. 技术选型:为什么是 Spring Boot 3 + MyBatis-Plus,而不是 JSP + Servlet
2.1 从传统 Java Web 到 Spring Boot:这套技术栈解决的核心痛点
如果放在十年前,基于Java Web的药店管理系统的主流方案是JSP + Servlet + JDBC,数据库连接自己管理,事务自己处理,页面用JSP脚本渲染。这套方案放在今天,不是说不能做,而是太痛苦了:开发效率低、配置繁琐、前后端耦合严重、难以测试。
Spring Boot解决了两个最核心的问题:自动配置和开箱即用。引入依赖后,内嵌的Tomcat帮你把Web容器启动了,application.yml里简单几行配置就能连上数据库,再也不用手写web.xml和各种XML配置文件。
我在这套系统里选择的是Spring Boot 3.2.x + JDK 17 + MyBatis-Plus 3.5.x + MySQL 8.x。这组组合是目前大多数新项目的默认起点。
注意:Spring Boot 3.x 要求 JDK 17 及以上,不要再用 JDK 8 跑 Spring Boot 3 项目,Maven 编译阶段就会直接报错。如果你电脑上装的是 JDK 8,要么换 JDK 17,要么把 Spring Boot 降级到 2.7.x,二选一,别硬刚。
2.2 MyBatis-Plus 带来的效率提升:少写大量重复 SQL
药店管理系统有大量单表CRUD操作,如果用原生MyBatis,每写一个实体类就要配套一个Mapper接口和一个XML文件,里面是大量的insert、update、delete、selectById模板语句。纯手写这些东西,机械重复不说,还容易在字段名对上对下时出错。
MyBatis-Plus的核心价值是:继承BaseMapper<T>之后,常用的单表操作直接就有了,不需要写任何SQL。对应的Service层继承IService<T>,还能拿到saveOrUpdate、lambdaQuery、page这些现成的封装方法。
举个实际例子,药品的分页多条件查询,用MyBatis-Plus写起来非常简洁:
public Page<Drug> pageDrugs(int pageNum, int pageSize, String keyword, Long categoryId, Integer status) { Page<Drug> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Drug> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Drug::getName, keyword) .eq(categoryId != null, Drug::getCategoryId, categoryId) .eq(status != null, Drug::getStatus, status) .orderByDesc(Drug::getCreateTime); return drugMapper.selectPage(page, wrapper); }这一整段代码,如果用XML手写,你得先去拼接动态SQL条件,再处理分页方言(MySQL的LIMIT),再写count查询,一页代码瞬间翻倍。而这里20行以内全部搞定,还不容易出错。这就是我选MyBatis-Plus最直接的原因——把精力留给真正有业务逻辑的地方。
2.3 前端方案:Thymeleaf 还是前后端分离
这个问题每次做项目都会被问。药店管理系统这个题目,我给的判断标准很简单:如果时间紧、想尽快跑通全流程,用Thymeleaf模板引擎;如果打算在简历上写成“前后端分离项目”,用Vue + Element Plus + Axios。
Thymeleaf的优势是服务端渲染,页面数据在后端拼好直接返回HTML,不需要考虑跨域、Token传递这些前后端分离才有的麻烦。缺点也很明显:页面和Java代码逻辑耦合,前端改动必须重启后端,交互体验相对粗糙。
前后端分离的优势是接口和页面完全解耦,以后扩展小程序、App端可以直接复用后端接口。代价是你需要同时维护两套工程,部署时处理静态资源,还得解决跨域和登录状态传递。
我最终选的是Thymeleaf + AdminLTE/Bootstrap的组合,原因很实际:这是课程设计级项目,核心考核点是Java后端逻辑和数据库设计,页面美观度通过现成模板可以快速补齐。Thymeleaf还能直接用th:each遍历数据渲染表格,比纯前端拉取接口再拼接DOM省事太多。
如果你确实想走前后端分离,我建议后端只写RESTful接口,返回统一结果集(Result<T>),前端用Vue3 + Element Plus + Vite来搭,登录状态用JWT + Axios拦截器处理。这条路能走通,但准备时间基本要翻倍,你自己权衡。
3. 数据库设计:药品库存这类业务的表结构要避开哪些坑
3.1 核心表结构设计思路
数据库是一套管理系统真正的地基。药店管理系统我最终设计了8张核心表,这里挑重点的讲:
用户表(sys_user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录名,唯一 |
| password | varchar(100) | BCrypt加密后的密文 |
| real_name | varchar(50) | 真实姓名 |
| role | varchar(20) | ADMIN / MANAGER / CASHIER |
| status | tinyint | 1启用 0禁用 |
密码存储这一点必须强调:明文存储是管理系统项目中非常严重的安全缺陷。哪怕只是课程设计也应该用BCryptPasswordEncoder加密后再存库。
药品表(drug)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| drug_code | varchar(50) | 药品编码,唯一 |
| name | varchar(100) | 药品通用名 |
| category_id | bigint | 分类ID |
| manufacturer | varchar(100) | 生产厂家 |
| specification | varchar(100) | 规格,如0.25g*24片 |
| unit | varchar(20) | 单位,盒/瓶/袋 |
| sale_price | decimal(10,2) | 零售价 |
| purchase_price | decimal(10,2) | 进货价 |
| prescription_type | tinyint | 处方药/非处方药 |
| status | tinyint | 上架/下架 |
| create_time | datetime | 创建时间 |
库存批次表(stock_batch)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| drug_id | bigint | 药品ID |
| batch_no | varchar(50) | 批次号 |
| supplier_id | bigint | 供应商ID |
| quantity | int | 当前库存量 |
| production_date | date | 生产日期 |
| expire_date | date | 有效期至 |
| create_time | datetime | 入库时间 |
销售主表(sale_order)和销售明细表(sale_order_item)
这两个是典型的主从表结构。主表记录一笔销售的整体信息:单号、总金额、收银员、销售时间;从表记录这笔销售的每一行药品明细:药品ID、数量、单价、小计金额。
为什么要拆两张表?因为一笔销售单包含多种药品,如果把药品信息直接存主表里,就得在一个字段里存很多内容的冗余结构,后面统计、退货全部没法做。拆成主从表之后,按单号查明细、按药品统计销量,都是最简单的SQL。
3.2 为什么库存一定要按批次管理
这是这个项目里相当关键的一个设计决策。
假设进货价在变,同一款药,不同时间从不同供应商那里进,价格可能不一样。如果库存表只存一个总数,那么销售出库时,你根本不知道卖出去的这批药是哪个批次进的、什么时候到期。药店的实际场景里,药品有效期监管是硬性要求,过期的药不能卖、临期的药要预警,这些全都依赖批次维度。
所以我的设计是:药品主数据里只存药品的基本信息和默认售价,库存总数通过stock_batch表动态汇总。每次采购入库,新增一条或多条批次记录;每次销售出库,按先进先出(FIFO)原则优先扣除最早到期的批次数量。
批次表的好处有三个:
- 能知道当前库存分布在哪些批次、各有多少
- 能计算哪些批次临近有效期,做临期预警
- 采购退货时能精确定位到某一批次
这个设计在答辩时只要讲出来,老师基本就知道你不是第一次做管理系统。
3.3 金额与数量的类型选择:BigDecimal 和整数类型的取舍
这里分享一个小经验。钱的字段,零售价、进货价、总金额,一律用 decimal(10,2) + BigDecimal,绝对不要用 float 或 double。这不是教条,而是因为二进制浮点数在计算机里表示十进制小数时有精度误差。你去算一笔0.1 + 0.2,结果不是0.3而是一长串小数,这在涉及钱的系统里是不可接受的。
数量字段则用 int 或者 bigint。很多人习惯把库存量和价格一样用decimal,这是没必要的,药品的销售单位是盒/瓶/袋,都是整数,用整数类型还能避免0.5盒这种语义不清的状况。
还有一点和金额相关的建议:明细表和主表都要存当时的价格快照。什么意思?你录入一笔销售单时,应该把当时这批药的单价同步到明细表里,而不是在明细里只放一个 drug_id,查询时再去药品表里 join 价格。因为药品的零售价是允许调整的,如果价格改了,历史销售单里的价格也会跟着变,数据就失真了。价格快照这个设计虽然只多了一步操作,但对数据的真实性有很大意义。
4. 核心模块实现拆解:登录、库存预警、销售结账
4.1 登录模块:从 Session 到 JWT 的演进,以及我的取舍
前后端不分离的前提下,最稳妥的方案是Session + HttpSession 拦截器。用户登录成功后,把用户ID、用户名、角色存进Session,再写一个HandlerInterceptor,在请求进入Controller之前检查Session里有没有用户信息,没有就重定向到登录页。
如果用Spring Security,功能确实更强,但配置复杂度也更高。对这个项目来说,一个拦截器加一个@Aspect或者拦截器注解,已经完全够用了。
这里我分享一个细节:拦截器校验权限时,不要只判断“是否登录”,还要判断“角色是否匹配”。举个具体场景:收银员登录后,不应该能访问用户管理相关的URL。如果只做了登录校验、没做角色校验,那任何登录用户都能访问所有页面,权限分级就形同虚设了。
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("/login"); return false; } // 进一步校验角色权限 String uri = request.getRequestURI(); if (uri.startsWith("/admin") && !"ADMIN".equals(loginUser.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }注册这个拦截器的时候要注意排除静态资源:/js/**、/css/**、/images/**、/login这些路径不能拦截,否则页面样式全加载不出来,登录页也进不去。
4.2 库存预警:写一条有效的 SQL 比堆代码更重要
库存预警是药店系统的特色功能,也是很多同学容易做得“假模假式”的地方。
预警分两种维度:数量预警和效期预警。
数量预警的逻辑很简单:当某药品当前批次总库存低于设定的最低库存阈值时,列出该药品。实现时,要么在药品表加一个min_stock字段,要么在分类表里统一配置。我选了前者,因为不同药品的安全库存差异很大,比如急救药可以备多些,常规药少备一些,统一配置不符合实际情况。
效期预警才是药店系统真正区分于普通进销存系统的地方,它要查的是:“有哪些批次的药品在90天内即将到期,或者已经过期?”SQL可以这样写:
SELECT b.drug_id, d.name, b.batch_no, b.quantity, b.expire_date, DATEDIFF(b.expire_date, CURDATE()) AS remain_days FROM stock_batch b LEFT JOIN drug d ON b.drug_id = d.id WHERE b.quantity > 0 AND b.expire_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 90 DAY) ORDER BY b.expire_date ASC;如果希望把已过期批次的也展示出来,把BETWEEN条件换成b.expire_date <= DATE_ADD(CURDATE(), INTERVAL 90 DAY)即可。
这段SQL的核心不是语法有多难,而是把“临期判断”这个业务规则明确地翻译成数据条件。写之前先问自己:多少天内算临期?已过期要不要显示?过期批次能不能继续销售?这三个问题没想清楚,SQL永远写不对。
回到代码层面,预警的展现方式也有讲究。我的做法是在管理后台首页放一个“预警看板”:库存不足的药品数量、临期药品数量、已过期药品数量,用三个数字卡片展示,点击进去是具体列表。这么做数据一目了然,比单纯在列表页加一个筛选按钮更有“系统”的感觉。
4.3 销售结账:事务、扣库存、并发,一个都不能少
销售结账是整个系统里业务复杂度最高的操作,没有之一。它的流程不是简单插入一条记录,而是由一个完整业务操作组合起来的:
- 创建销售主单(sale_order),计算总金额
- 遍历购物车中的每一项药品,生成销售明细(sale_order_item)
- 校验每种药品的库存是否充足
- 扣减对应批次的库存
- 如果库存不足,整个销售操作要回滚
这几步操作必须在一个数据库事务里完成,否则就会出现“销售单创建了,库存却没扣”或者“库存扣了,销售单却没保存”这两种数据不一致的问题。
Spring 里用@Transactional就可以实现声明式事务,加在Service方法上即可。但有几个坑需要注意:
第一个坑:事务不生效。@Transactional加在非public方法上不生效;通过this调用同类内的另一个@Transactional方法也不生效。所以销售开单的逻辑要放在独立的Service类中,从Controller或外部Service调用,不要自己调自己。
第二个坑:库存扣减的并发问题。如果两个收银员同时卖同一盒药,各自的请求都读到库存=10,然后都执行UPDATE stock_batch SET quantity = quantity - 1,就可能导致超卖。简单有效的做法是用乐观锁:更新库存的SQL加上数量条件。
UPDATE stock_batch SET quantity = quantity - #{count} WHERE drug_id = #{drugId} AND batch_no = #{batchNo} AND quantity >= #{count}这里quantity >= #{count}这个条件特别重要,它保证扣减操作自带“余额校验”,如果库存不够,这条更新影响的行数就是0,代码里通过int rows = stockBatchMapper.update(...)判断rows == 0就知道该批次库存不足或已被并发修改,然后抛异常触发回滚。
**第三个坑:重复提交。**用户结账时如果网络卡顿,可能连点两次“确认结账”,产生两笔一模一样的销售单。这个问题最基础的解法是前端按钮置灰(提交后禁用按钮),再进阶一点是后端做幂等控制,比如用唯一订单号做去重。课程设计阶段做到前端防重复点击就够了,但你要知道后面还有这道坎。
4.4 通用 CRUD 的自动化:MyBatis-Plus 的实用姿势
做管理系统,不要去一个模块一个模块地手动堆CRUD代码,那既累又没技术含量。配合MyBatis-Plus,正确的姿势是:
- 实体类加
@TableName、@TableId注解 - Mapper接口继承
BaseMapper<T> - Service接口继承
IService<T>,实现类继承ServiceImpl<M, T>
这样,药品、分类、供应商这几个基础模块的简单增删改查,几乎不用写任何SQL和业务代码,Controller里直接调用IService提供的方法就能完成。
重点讲一下逻辑删除。删除药品这个操作要慎重,因为药品一旦被销售单引用,物理删除会让历史数据变成“悬空引用”。我的做法是在表里加一个deleted字段,配合MyBatis-Plus的@TableLogic注解。删除操作变成更新deleted = 1,查询时框架自动过滤掉已删除的数据。这个做法在答辩时说“因为要保留历史业务数据的完整性”,是非常加分的回答。
创建时间和更新时间这两个字段也很常见,配合MetaObjectHandler可以实现插入和更新时自动填充:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }实体类对应字段上加@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.INSERT_UPDATE)即可。这个机制把大量业务代码从每张表的新增方法里解放出来了。
5. 从开发到部署:真实项目里绕不开的十个坑
5.1 Spring Boot 3 的版本兼容陷阱
搜索热词里有一句“springboot版本太高”,说的就是这个事。Spring Boot 3.0对比2.x,最大的变化是Jakarta EE命名空间迁移:javax.servlet变成了jakarta.servlet。很多旧教程、旧依赖在Spring Boot 3里会直接编译报错。
另一个常见坑是MyBatis-Plus版本匹配。MyBatis-Plus 3.5.x之后才支持Spring Boot 3(对应mybatis-plus-spring-boot3-starter这个依赖坐标)。如果你用的是Spring Boot 3,还引入老版本的mybatis-plus-boot-starter,启动时会直接报ClassNotFoundException之类的错误。
我吃过的亏是Spring Boot 3.2 + MyBatis-Plus 3.5.3.1这个组合,分页插件一切正常,但切换成3.5.5之后PaginationInnerInterceptor的配置略微有变化。所以我的建议是:确定一套版本组合,就不要再频繁升级。课程设计要求的是稳定跑通,不是追新。
5.2 数据库连接配置:时区、编码、SSL
连接MySQL时最容易踩的坑是时区问题。如果你在application.yml里这样写:
spring: datasource: url: jdbc:mysql://localhost:3306/pharmacy_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false这里serverTimezone不配置,或者配置成UTC,那么后端写入的时间会比北京时间早8个小时,所有的时间显示和统计都会错乱。useSSL=false是为了避免本地连接时SSL握手报错,生产环境建议反过来设置为true。
另外MySQL 8.x默认的驱动类是com.mysql.cj.jdbc.Driver,MyBatis-Plus一般能自动识别,但有些组合下需要手动指定,如果启动报驱动找不到,就加上这一行:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver5.3 分页查询和模糊搜索的性能坑
基础版的药品列表直接selectPage没问题,但数据量一旦上来,有几个细节要注意。
一是模糊查询别用%keyword%前导通配符,比如LIKE '%阿莫西林%'。这种写法在数据库层面无法走索引,数据量大了以后会全表扫描。但药店管理系统这个体量,一般几千条药品数据,不构成性能瓶颈,所以这里不用过度优化,知道有这回事就行。
二是我强烈建议你在列表页加上筛选条件:药品名称关键字、分类下拉、上下架状态。不要只做一个无筛选的列表然后靠前端分页。这个功能在答辩时很能说明你考虑了“管理场景下的操作效率”。
5.4 打包和部署:Maven 打包后运行不起来
本地IDEA里运行得好好的,一打包部署就出问题,这种情况遇到过不止一次。最常见的原因有:
- 静态资源路径问题:Thymeleaf模板里如果写了以
/开头的绝对路径,打包后放到子路径部署会404。统一用th:href="@{/css/app.css}"这类模板语法,让它自己拼上下文路径。 - 端口冲突:服务器上8080端口被占用,启动失败。打包前检查
application.yml里的server.port,部署时用--server.port=8081这种命令行参数动态覆盖最省事。 - 数据库连接不上:本地连的是
localhost,服务器上连的是另一台机器的数据库,打包前记得把application.yml里的数据库地址改成服务器能访问的地址。
一个更推荐的部署方式是用Maven插件打成Fat Jar,然后在命令行里用java -jar pharmacy-system.jar直接启动。这在云服务器上配合systemd或者简单的shell脚本就能做到守护运行。前端资源因为是内置的,不存在跨域问题,比前后端分离部署简单很多。
6. 这个项目的下一步:从课程设计到可上线的小微系统
6.1 功能扩展:会员、报表、批号追溯
如果你做完核心功能之后还有余力,我建议优先往这三个方向扩展。
会员管理是很多药店的刚需。可以加上一张member表,销售开单时选择会员,消费金额累计积分,按积分分级打折。这部分涉及的功能不复杂,但对业务完整度的提升非常明显。
统计报表是另一个性价比高的扩展方向。用ECharts展示近7日销售额折线图、分类销售额饼图、畅销药品Top10柱状图。数据来源就是销售明细表,SQL写几个GROUP BY查询就行。页面效果却非常出彩,答辩演示时看着也专业。
批号追溯是更进阶的方向。每一笔销售明细,除了记录药品ID,再记录批次号。这样就能回答“这盒药是哪个批次进的、哪一天卖给了哪个会员”这个问题。真实药店在药品召回时必须要有这个能力,这是进销存系统数据完整性的高级体现。
6.2 安全增强:密码加密、SQL注入、XSS防护
虽然课程设计对安全没有强制要求,但如果你简历上写了这个项目,面试官一定会追着安全问题问。我在这里说三个能立刻落地的安全措施。
首先是密码加密。不要用MD5,MD5已经可以被彩虹表秒破。用Spring Security自带的BCryptPasswordEncoder,每次加密自动加盐,同一个密码两次加密结果不同,这才能算合格。
其次是SQL注入防御。MyBatis的#{}是预处理参数占位,能有效防止SQL注入;但如果你图方便在XML里写了${}直接拼接参数,那就是一个典型的注入点。原则很简单:能用#{}就别用${}。
再就是XSS防护。用户输入的药品名称、供应商名称如果直接渲染到页面上,可能携带恶意脚本。最简单的做法是在后端统一对请求参数做转义过滤,或者在前端模板里对输出做escape。Thymeleaf默认对HTML标签是转义的,这本身就是一层保护,不要轻易用th:utext输出用户输入的内容。
6.3 写文档和答辩:让老师看到你的设计思想
做这个项目到最后,文档和代码同样重要。我不建议把README写成“怎么启动项目”的工具说明,而是应该把系统设计思路讲清楚。
文档里我建议包含这样几个部分:
- 系统概述和角色分析(这三种角色分别能干什么)
- 核心业务流程图(采购入库、销售出库两条链路)
- 数据库ER图和设计说明(重点讲为什么库存要按批次管、为什么销售要拆主从表)
- 核心难点解答(并发扣库存怎么解决、临期药品怎么预警)
答辩时,最忌讳的是照着PPT念功能列表。你先讲清楚“药店业务有哪些环节”,再说“我的系统怎么覆盖这些环节”、“哪些环节是重点难点”、“怎么解决的”,这一套逻辑下来,比单纯演示页面要加分得多。
我在检查最终代码前的习惯是:把整个项目从零开始删除重构,至少跑通一遍mvn clean package加java -jar启动。如果你能做到这一步,从部署到答辩的每一个环节,你都心里有底。这套方法在我做过的管理系统项目里一直沿用,项目能不能交付,从来不是看代码写了多少,而是看整个链路跑了几遍。