☰
JSP基于Java的在线药店管理系统毕业设计全流程解析
2026/10/20 17:17:53 网站建设 项目流程

每年三月到五月,我都会收到一批类似的毕业设计咨询:“老师,题目里写的是JSP基于JAVA的在线药店管理系统,这个还能做吗?会不会太老?”我的回答一直没变:能做,而且这是最适合用来完整走一遍Java Web开发流程的题目之一。它的业务链条清楚——从前台用户浏览药品、加入购物车、下单,到后台管理员管理药品、处理订单,整套流程五脏俱全,没有任何多余的想象力负担。更关键的是,这套系统的技术组合JSP + Servlet + JavaBean + MySQL + Tomcat,几乎是各大高校软件技术类专业的课程设计、毕业设计标准配置,也正好把大学三年学的Java基础、数据库原理、Web基础全部串起来。

这篇博文我想从选题逻辑、技术选型、数据库建模、核心功能实现、部署调试五个维度,把在线药店管理系统从头到尾拆一遍。它的价值在于:你拿到这个题目后,不是到处找代码、换个首页图片就交差,而是真正理解每一行代码要解决什么业务问题,每一张表的存在理由是什么,面试官或答辩老师追问时你能从容答上来。

1. 为什么“在线药店管理系统”是毕设选题里的常青树

1.1 一个业务闭环完整的题目自带“可答辩性”

毕设答辩最怕的事情,是评委老师问一句“你的系统解决了什么问题”,学生答不上来。很多花里胡哨的选题,比如“基于XX的智能推荐系统”“基于XX的可视化平台”,看上去很高级,但实际做下来往往是调包、调接口、拼数据,最后连核心逻辑都说不清楚。药店管理系统不会出现这种情况。

把“在线药店”四个字拆开看,它涉及两个完全不同的使用角色:前台买药的顾客,后台管药的运营人员。顾客要做的事很朴素——注册、登录、浏览药品、搜索、加购物车、下单、模拟支付、查看订单;运营人员要做的事也很直接——药品分类管理、药品上下架、库存维护、订单状态推进、用户信息查看。这两个角色之间被订单这条主线串起来,刚好构成一个完整的管理信息系统闭环。

这个闭环意味着什么?意味着你的系统天然具备三层业务逻辑:数据层(药品、用户、订单、库存)、业务层(下单时校验库存、扣减库存、生成订单)、表现层(前台页面、后台页面)。任何一个有经验的评委,看到这个结构都清楚你的工作量在哪里,不会故意为难你。

1.2 业务规则里藏着真正的“含金量”

药店不是普通商城,医药电商有自己特殊的业务规则,这些规则就是你答辩时的加分项。举例来说,药品有处方药和非处方药的区别,处方药在页面上必须做标识,需要“凭方购买”的提示逻辑;药品库存是有限且不可为负的,下单时必须校验;药品存在有效期概念,后台需要维护生产日期和有效期字段,下架逻辑要考虑临近效期的情况。

能在系统里把这类行业细节做出来,哪怕只是做了字段设计层面的约束,也会让评委觉得你“认真调研过需求”,而不是随便拿个商城模板改了个名字。另一个隐藏加分点是订单状态流转,药店订单通常有“待支付→待发货→已发货→已完成→已取消”这样的状态机,你要在后台提供按状态筛选订单的功能,并在前台显示当前订单的实时状态,这种设计非常符合真实业务场景。

1.3 适合不同水平的学生作为毕设起点

这个题目还有一个好处:可难可易。基础薄弱的同学,可以把核心功能做成“商品展示+购物车+下订单+后台管理”四大模块,配上简洁的页面,就能顺利答辩;基础不错的同学,可以在上面继续叠加用户收货地址管理、订单状态机、库存预警、数据统计图表、Ajax异步校验用户名、验证码登录等进阶功能。同一个选题,做成什么样完全由你自己掌控,不存在“题目太难做不完”的尴尬。

我经常跟学生说一句话:毕业设计挑题,挑的不是“看起来最厉害的”,而是“你最有把握讲清楚来龙去脉的”。在线药店管理系统恰恰就是这种“你每一个功能都能讲明白为什么这么做”的题目。

2. 技术选型:JSP + Servlet这套“老组合”在今天为什么仍然成立

2.1 选型之前先看清学校的评分标准

现在网上铺天盖地都是Spring Boot + Vue的教程,很多学生上来就问能不能用Spring Boot做。这里有一个很现实的问题:很多高校的计算机专业,Java Web课程教的就是JSP + Servlet + JavaBean三层架构,毕业设计题目直接沿用课程知识体系。你如果用Spring Boot重写,第一是偏离了题目的JSP要求,第二是答辩时老师会更严格地追问框架原理,反而可能露怯。

被明确要求用JSP写的同学,也不要觉得这套技术过时了。JSP + Servlet的思想内核是MVC,而MVC是所有现代Web框架的鼻祖。你把这个项目的Servlet、JSP、JavaBean分工搞清楚,写清楚它们各自扮演的角色,以后出去写Spring MVC,你会发现那只是同一套思想的封装而已。

2.2 完整技术栈与每层选型的理由

一套可运行的药店管理系统,具体技术栈大致如下,我顺带把选择理由列在表格里:

层次技术组件选择理由
前端页面JSP + JSTL + EL表达式服务端渲染,天然契合JSP题目要求;JSTL和EL让JSP页面里尽量不写Java脚本,保持结构整洁
控制层Servlet负责接收请求、调用业务方法、分发JSP页面,是整个MVC的调度中心
业务层JavaBean / Service类把业务规则写在独立类里,比如下单、扣库存、登录校验,避免把业务代码堆在Servlet里
数据层DAO + JDBC / 连接池每张核心表对应一个DAO,完成增删改查;连接用DBCP连接池或自封装JDBCUtil
数据库MySQL 5.7 / 8.0轻量、教学资源多,可视化工具丰富(Navicat、DataGrip都可以)
服务器Tomcat 8.5/9.0JSP项目的标准运行容器,部署方式简单,把项目打成war包丢进webapps即可
前端样式Bootstrap / Layui 或简单CSS药店管理系统属于管理型系统,界面重心是表格和表单,没必要上重前端框架

这里想多说一句DAO层的事。很多同学喜欢用DBUtils或者MyBatis,但在这个题目下,我建议至少亲手写两个表的完整JDBC增删改查再考虑封装。因为毕业设计考察的就是你对基础原理的掌握程度,你亲手写过PreparedStatement、ResultSet遍历、连接关闭的顺序,答辩时被问“JDBC查询分几步”,你能脱口而出:加载驱动、获取连接、预编译SQL、设置参数、执行查询、处理结果集、关闭资源。

2.3 三层架构如何在项目中物理落地

项目结构上,我推荐按下面这个包结构组织代码:

src/main/java ├─ com.xxx.pharmacy │ ├─ entity // 实体类:User, Drug, Category, Order, OrderItem... │ ├─ dao // 数据访问层:UserDao, DrugDao, OrderDao... │ ├─ service // 业务层:UserService, DrugService, OrderService... │ ├─ servlet // 控制层:LoginServlet, RegisterServlet, CartServlet... │ ├─ filter // 过滤器:编码过滤器、登录验证过滤器 │ └─ util // 工具类:DBUtil, StringUtil... src/main/webapp ├─ 前台页面 │ ├─ index.jsp // 药品列表首页 │ ├─ login.jsp // 用户登录 │ ├─ register.jsp // 用户注册 │ ├─ detail.jsp // 药品详情 │ ├─ cart.jsp // 购物车 │ ├─ order.jsp // 确认订单 │ └─ userCenter.jsp // 个人中心/我的订单 └─ 后台页面 ├─ adminLogin.jsp ├─ drugManage.jsp // 药品管理 ├─ categoryManage.jsp // 分类管理 ├─ orderManage.jsp // 订单管理 └─ adminIndex.jsp // 后台首页

三层架构在物理上的体现就是:Servlet不直接连数据库,它调用Service;Service调用DAO;DAO里只有SQL操作;JSP通过EL表达式从request或session域拿数据,不写Java脚本。这张边界只要分清楚,整个项目的代码质量就已经超过大半同行了。

3. 数据库设计:药店系统的表和字段是这么定出来的

3.1 核心表结构清单与设计思路

数据库是一切的底座。很多同学上手就建表,想到什么加什么字段,结果写到购物车发现订单表缺状态,写到后台发现药品表缺上下架字段,来回改,痛苦至极。我建议第一步先把业务过程画清楚:用户产生订单,订单包含多个药品条目,每个条目对应一个药品;药品归属某个分类;用户下单前把商品放在购物车里。

按照这个关系,核心表基本固定了下来:

表名职责关键字段说明
user前台用户id, username, password, phone, address, create_time注册与登录;address用于收货
admin后台管理员id, username, password与用户表分开存,避免角色混乱
category药品分类id, name, description如感冒用药、肠胃用药、皮肤用药
drug药品信息id, category_id, name, spec, manufacturer, image, price, stock, prescription, description, shelf_status核心字段多,price用DECIMAL,stock用INT,prescription标记是否处方药
cart购物车id, user_id, drug_id, quantity, add_time关联用户与药品,可以用唯一约束(user_id, drug_id)防止重复记录
orders订单主表id, order_no, user_id, total_price, status, consignee, phone, address, create_timeorder_no用时间戳+随机数生成唯一订单号;status存状态码
order_item订单明细id, order_id, drug_id, drug_name, price, quantity冗余drug_name和price快照,因为后续药品价格可能变动

有一个细节我想特别提醒:订单明细表里的药品名称和价格一定要做冗余存储。这不是数据库设计洁癖,而是真实业务必须如此——用户在购买时看到的“感冒灵颗粒 12.8元”,要在他下单后保留在那个时间点的状态。如果你去关联查询drug表,过几天药品改价了,用户查看历史订单价格也跟着变了,这在业务上是严重的错误。

3.2 外键加不加?这是一个答辩常问点

外键约束在教科书里非常受推崇,但实际开发中很多团队会刻意减少外键,改由应用层控制数据一致性。毕设项目里,我建议在数据库设计文档中画出表关系图并标注外键字段,但在建表SQL里,可以保留逻辑外键关系,用索引替代物理外键约束。原因很简单:物理外键会限制插入顺序,测试时造数据非常麻烦;而逻辑外键让你在DAO里写SQL时更自由,也不影响你答辩时讲解表关系。

比如order_item表里的drug_id,它逻辑上引用drug表的id,但我建表时不会加FOREIGN KEY,只是对drug_id加一个普通索引。真被问到“为什么不用外键”,你的回答可以很专业:应用层事务可以保证一致性;物理外键在高并发插入时不友好;物理外键会让删除药品时被迫先处理子表,增加复杂的级联逻辑。这个回答的水平,远超“别人说不用就不用”。

3.3 订单号、金额精度与状态字段的硬性约束

订单号和金额是两个容易翻车的字段。订单号我建议在service层生成,格式可以简单定为“yyyyMMddHHmmss + 4位随机数”,存成varchar;金额必须用DECIMAL(10, 2)而不是FLOAT或DOUBLE,理由很直接:二进制浮点数表示十进制小数有精度误差,算购物车总价时1.1+2.2可能得到3.3000000000000003,这在涉及钱的项目里是绝对不允许的。Java侧对应使用BigDecimal,不要用double累加。

订单状态我建议用一个TINYINT字段存状态码,把状态含义在代码常量类里维护,而不是直接存中文。这样设计的好处是后续做状态筛选和状态机流转非常方便。我一般定这样的状态规则:

public class OrderStatus { public static final int WAIT_PAY = 0; public static final int WAIT_SEND = 1; public static final int SENDED = 2; public static final int FINISHED = 3; public static final int CANCELLED = 4; }

这张表建好之后,前台的购物车和后台的药品管理就都有据可依了。数据库设计阶段不要急着写代码,反复对着表结构走一遍业务流程,发现“下单时要更新哪个表”“取消订单要改哪些状态”“药品下架对购物车有什么影响”,这些问题想清楚再动手写代码,效率会高很多。

4. 核心功能实现:登录、购物车、下单这些关键代码到底怎么组织

4.1 登录与注册:会话管理的正确姿势

用户登录是整个前台功能的前提。登录逻辑并不复杂:前端表单提交用户名和密码,LoginServlet接收参数,调用UserService的login方法,查询数据库比对密码,成功则把user对象放进session,失败则返回错误提示。

注意,密码绝对不要用明文存储。虽然这是毕设项目,但从一开始养成好习惯:注册时用MD5或SHA-256哈希后再入库,登录时也哈希再比对。

下面给出核心的登录逻辑代码结构:

@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

这个代码里有几个值得在答辩时展开的点。第一个是重定向和转发分别用在什么场景:登录成功后用sendRedirect,是为了避免刷新页面重复提交表单;登录失败用forward,是因为要把error信息带到JSP页面显示。第二个是session的作用域,购物车、登录用户这些数据放session,药品列表、搜索结果这些数据放request即可,不要所有数据都往session里塞。

用户登录之后,整个前台页面顶部显示“欢迎 xxx,退出登录”这些状态变化,用JSTL判断session里是否有loginUser就能完成。与此同时,后台管理员登录可以独立做一个adminLogin.jsp和AdminLoginServlet,两张表互不干扰,这样逻辑更清晰。

4.2 药品展示与分页:首页、列表页、详情页的串联

药品展示是用户对系统的第一印象。首页推荐位可以做个简单的查询,比如“上架时间最近的8个药品”,后台管理人员在药品列表页可以点击“推荐”标记,也可以用字段is_recommend来控制,这是很常见的拓展。

分页是必考必问的技术点。分页的原理特别简单:MySQL的LIMIT关键字,配合两个参数currentPage和pageSize。查询总记录数算总页数,当前页数据用LIMIT offset, pageSize取回来,前端JSP通过循环渲染。我可以直接给出一个分页的核心思路:

int pageSize = 8; int currentPage = req.getParameter("page") == null ? 1 : Integer.parseInt(req.getParameter("page")); int totalCount = drugDao.getTotalCount(); int totalPages = (int) Math.ceil(totalCount * 1.0 / pageSize); int offset = (currentPage - 1) * pageSize; List<Drug> drugList = drugDao.findByPage(offset, pageSize);

这里面有一个细节容易栽跟头:页码参数从前端传过来时,一定要处理“第一页”“最后一页”的边界,也就是currentPage不能小于1,也不能大于totalPages。否则用户手动改URL的page参数,页面就会报错。做一个简单的越界修正处理,比什么都强。

药品详情页相对简单,通过drugId查出药品的对象,展示名称、规格、生产厂家、价格、库存、用法说明等。这里要注意创建一个PageUtil工具类封装分页逻辑,前台药品列表、后台药品管理、用户订单列表都用得上,避免三处各写一遍分页代码,这是减少重复代码的常用套路。

4.3 购物车功能的两种存储方案,各有利弊

购物车是整个系统里设计弹性最大的模块,我见过两种主流方案。

第一种,购物车数据放Session。用户点击“加入购物车”,Servlet在session里取一个Map<drugId, quantity>,塞进去再setAttribute回session,购物车页面遍历这个Map显示。好处是逻辑简单,不需要额外建表,刷新不会丢;坏处是用户清浏览器缓存或session过期购物车就没了,而且不方便后台统计用户加购行为。

第二种,购物车数据存数据库cart表。前端每个操作都调用Servlet去cart表做增删改查,页面加载时从表里查出来展示。好处是数据持久,换设备登录也能看到;坏处是每个操作都要走数据库,代码量明显增加,而且会有未登录用户不能使用购物车的问题。

对于毕业设计项目,我倾向于推荐数据库方案。因为cart表建好了,前台购物车页面和订单模块天然能串起来,工作量增加有限,但整体系统完整度会提升一个档次。未登录用户点击“加入购物车”时,拦截器或页面判断直接跳转登录页,这个体验也符合真实系统逻辑。

加入购物车的核心Servlet代码如下:

int drugId = Integer.parseInt(req.getParameter("drugId")); int quantity = Integer.parseInt(req.getParameter("quantity")); User loginUser = (User) req.getSession().getAttribute("loginUser"); CartDao cartDao = new CartDao(); CartItem item = cartDao.findByUserIdAndDrugId(loginUser.getId(), drugId); if (item == null) { cartDao.add(new CartItem(null, loginUser.getId(), drugId, quantity, new Date())); } else { item.setQuantity(item.getQuantity() + quantity); cartDao.update(item); } resp.sendRedirect(req.getContextPath() + "/cart");

这段代码里隐藏着一个必须处理的问题:同一种药品重复加入购物车,要做“数量叠加”而不是“新增一行记录”。很多同学一开始没注意,结果用户点了两次加入,购物车里有两行同一个药,结算时数字完全不对。库存校验也可以在这里先做一次:如果药品当前库存小于加入数量,直接提示“库存不足”。

4.4 后台管理:药品上传与管理功能怎么落地

后台管理是整个系统的另一大块,核心是药品的增删改查,外加分类管理、订单管理、用户管理。药品图片的处理值得单独说一下——不要走复杂的文件上传框架,用Tomcat自带的part机制就能搞定:form表单设置enctype="multipart/form-data",Servlet里用getPart("image")拿文件,把图片保存到服务器指定的上传目录,然后把图片的相对路径存到drug表的image字段。

Part part = req.getPart("image"); String fileName = UUID.randomUUID().toString().replace("-", "") + ".jpg"; String basePath = getServletContext().getRealPath("/upload"); part.write(basePath + File.separator + fileName); drug.setImage("upload/" + fileName);

这里有个经验:图片文件名一定要用UUID重新生成,千万不能直接用用户上传的原始文件名。原因很现实——不同用户上传的图片可能同名,比如都是1.jpg,后面传的会把前面传的覆盖掉;再有中文文件名会导致页面显示乱码或路径问题。UUID重命名是通用做法,成本极低,受益无穷。

后台药品管理的页面通常是表格加搜索框、分页、操作列的组合。核心字段展示有药品名称、分类、价格、库存、是否上架、创建时间。操作列放“编辑”“上下架”“删除”,本质上就是写三个Servlet对数据库做更新。这个模块要注意:删除药品之前检查有没有在售订单关联,如果order_item里存在该drug_id的记录,只能做下架处理,不能物理删除,否则历史订单会显示不完整。这个细节很多同学想不到,但在真实业务里是强制约束,它会成为答辩时的加分亮点。

5. 订单流程:从购物车到生成订单到模拟支付的一整套事务逻辑

5.1 确认订单页的数据组装:展示什么、提交什么

购物车页面点击结算,系统先跳到确认订单页。这个页面要把本次要结算的商品明细展示一遍,同时让用户填写或确认收货人、手机号、地址,总金额在页面底部汇总展示。

组装这页数据的核心逻辑是:根据当前登录用户的id查cart表,拿到所有购物车条目;逐个条目关联drug表查药品最新价格和库存;在service层计算总金额。页面展示时,特别注意商品数量不能大于库存,如果库存不足,要么提示修改数量,要么把该商品标红提示。这一步是体验问题,也是数据正确性问题。

用户在确认订单页点击“提交订单”时,表单不仅要提交收货人信息,还要把“本次结算的购物车条目id列表”一并提交。我的实现方式是:cart.jsp页面的每行都有一个checkbox,结算按钮只提交选中的条目。这样用户可以挑选部分药品下单,而不是一结算必须把整个购物车清空。

5.2 下单为什么必须用数据库事务

下单这个动作,在数据库层面不是一个INSERT,而是好几个操作捆在一起:

  1. 往orders表插入一条订单记录;
  2. 往order_item表插入若干条订单明细;
  3. 扣减drug表里对应药品的库存;
  4. 从cart表删除本次结算的购物车条目。

这四步必须要么全部成功,要么全部失败。如果只插入订单,没扣库存,那库存数据就失真,后台看到有库存、实际已经卖掉了;如果扣了库存但没生成订单,那顾客就白付了钱。解决这个问题的答案就是数据库事务:同一Connection上执行多个更新,最后统一commit或rollback。

Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. insert order // 2. insert order items // 3. update drug stock // 4. delete cart items conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException("下单失败", e); } finally { DBUtil.close(conn); }

这里有一个直接决定项目成敗的细节:不要在这四个操作之间关闭 Connection。DAO里的方法你设计成接收外部传入的Connection,而不是自己在内部new一个,这样事务才能串在同一个连接上。实现方式可以给DAO层写方法签名时带上Connection参数,比如drugDao.updateStock(conn, drugId, quantity)。这就好比四个人共用同一支笔在签合同——中途换笔,合同就签不成。

下单完成之后,系统跳转到支付页面,由于是毕业设计,不接真实支付接口,模拟支付即可。支付页面显示订单号和应付金额,用户点击“确认支付”,Servlet把订单状态从WAIT_PAY更新为WAIT_SEND。这里你也可以设计成一个简单的“货到付款”选项直接确认订单,看学校要求是什么。

5.3 订单状态的流转与两个后台展示功能

订单生成后,后台管理员需要看到所有订单,并按状态筛选。订单列表页建议列出来的字段包括:订单号、用户账号、收货人、联系电话、总金额、状态、下单时间、操作。操作列里要有“查看详情”“发货”“取消订单”这些按钮。

状态流转的边界条件要提前想清楚:

  • WAIT_PAY状态下的订单,用户可以自行取消,也可以继续支付;
  • WAIT_SEND状态下的订单,用户不能自行取消,只能管理员发货或拒绝;
  • 管理员点击发货,状态改为SENDED;
  • 用户收到货后,可以手动点击“确认收货”,状态改为FINISHED;
  • 已完成的订单不允许再次修改状态。

这些流转规则在Servlet里用if判断实现,代码不复杂,关键是要全面。我见过不少项目只有一个简单的“发货”按钮,没有边界判断,用户把已发货订单又取消了,导致后台发货流程混乱。状态机的设计思路想清楚,后面的逻辑就顺了。

订单详情页是一个联表查询的经典案例:orders表关联user表取收货人信息,order_item表关联drug表取药品信息。它的SQL通常是这样:

SELECT oi.drug_name, oi.price, oi.quantity FROM order_item oi WHERE oi.order_id = ?

这个查询不需要join,因为order_item里已经冗余了drug_name和price。你在详情页展示历史价格时,用的是下单时的快照数据,而不是drug表当前价格,这个设计在前面数据库部分特别强调过,到订单模块终于体现出了价值。

5.4 并发超卖问题:毕设里怎么处理才合适

“超卖”是电商系统的高频考点,哪怕你是毕业设计,也值得理解这个概念。超卖是什么?就是库存只剩1件,但两个用户同时下单,两个人都扣到了库存1,最后都成功了,卖出了2件。原因在于两个请求同时查库存,都看到剩1,都通过了校验,然后各自扣减。

真实工业界解决超卖要靠数据库乐观锁或悲观锁。毕业设计项目里,最简单可靠的做法是使用SQL层面的原子更新:

UPDATE drug SET stock = stock - 1 WHERE id = ? AND stock > 0;

这条UPDATE自带条件:只有库存大于0时才会真正扣减,并且它是数据库层面的原子操作,两个并发请求同时执行,第二个会因为库存变为0而影响行数为0。代码里通过判断update影响的行数,就能知道是否扣减成功,失败则提示库存不足。这个写法比“先SELECT判断再UPDATE”可靠得多,代码量也不会增加。把这个原理写在设计说明书里,是绝对的加分项。

6. 项目部署与调试:从乱码到404,最容易被拖住的地方

6.1 乱码问题一网打尽:三层编码必须统一

JSP项目乱码是出现频率最高的问题,很多同学折腾半天没解决,最后重装了数据库。乱码的根源是“编码不统一”,解决思路就是让所有环节统一用UTF-8。需要检查的具体位置有四个:

第一,MySQL连接URL要加上字符编码参数:

jdbc:mysql://localhost:3306/pharmacy?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

第二,数据库表结构和字段的Collation要设置成utf8mb4_general_ci,建库时可以统一指定。第三,每个JSP页面顶部的page指令设置contentType="text/html;charset=UTF-8"和pageEncoding="UTF-8"。第四,由于POST请求默认编码是ISO-8859-1,所以要在Servlet里接收参数前设置请求编码:

req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8");

更省事的做法是写一个EncodingFilter,在web.xml里用/*拦截所有请求,统一设置编码,这样每个Servlet里就不用手写一遍了。这个过滤器代码量极小,但能根治整个项目百分之八十的中文乱码。

6.2 404、500与端口冲突的快速定位思路

JSP项目调试时最多的报错就三类:404页面找不到、500服务器内部异常、端口被占用。404要区分是JSP文件不存在,还是Servlet映射路径写错。Servlet如果用@WebServlet注解,urlPatterns要和前端表单action完全一致,比如前端是form action="login",后端注解就必须是@WebServlet("/login")。一个最常见的坑是把注解写成@WebServlet("/loginServlet"),前端却提交到"login",结果404。

500错误通常能在控制台看到具体异常栈。最高频的几类:ClassNotFoundException——JAR包没有放到WEB-INF/lib目录下;NullPointerException——查询结果为null没做非空判断;NumberFormatException——前端传了空字符串给parseInt。遇到500别慌,第一件事先把控制台的完整报错信息复制下来,按提示精确到哪一行,绝大多数问题都是小毛病。

端口被占用的场景,多半是之前Tomcat没关干净,再次启动报“Port 8080 required by Tomcat ... is already in use”。解决办法是找到占用进程并终止,Windows下命令是netstat -ano | findstr 8080,然后taskkill /pid 进程号 /f。这个操作熟练工和新生都能处理,但卡住的概率最高。

6.3 连接池与驱动JAR的典型配置

纯JDBC项目里,最常被问到的就是数据库连接怎么管理。最简陋的做法是每次Dao访问数据库都DriverManager.getConnection(),用完就关闭。这样做在小项目里不是不能用,但连接频繁开关很浪费资源,高并发场景会直接拖垮数据库。毕设项目我建议引入DBCP连接池,配置文件写在src目录下的dbcp.properties里:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/pharmacy?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxTotal=20 maxIdle=10 minIdle=5

工具类DBUtil在类加载时读取配置,初始化连接池,对外提供getConnection和close两个静态方法。注意这里的close方法如果拿到的是连接池的连接,调用close并不是真正关闭物理连接,而是把连接归还到池里,这个机制理解了能答很多拓展问题。

Tomcat的lib目录不能随便放JAR包,mysql-connector-java的驱动JAR必须放在项目的WEB-INF/lib目录下,这是初学者容易忽略的点——放在Tomcat/lib下虽然也能跑,但换台机器部署就失效了,而且依赖不清晰。

6.4 部署包的制作与演示环境的准备

所有功能都调试通过后,需要把项目导出成war包部署。IDEA里面直接Build -> Build Artifacts -> 选择Web Application: Exploded或Archive即可,把生成的war包复制到Tomcat的webapps目录下,启动Tomcat后访问http://localhost:8080/项目名/就能看到效果。

关于演示环境,还有一个非常现实的建议:正式答辩之前,数据库里一定要预置好演示数据——几十个不同分类的药品、带图片的药品信息、至少一个用户、几个不同状态的订单、后台管理员账号、购物车里有数据。很多同学答辩当场才发现数据库是空的,现敲数据又紧张,页面全是空白表格,效果大打折扣。预置演示数据不是造假,而是保证演示节奏连贯,让评委把注意力放在系统功能上。

7. 拓展功能推荐与我的个人心得

系统做完核心功能之后,如果时间和能力允许,我强烈建议加一两个有亮点的拓展功能。这些功能不一定要多难,关键在于“恰到好处”。

第一个推荐Ajax异步校验用户名。注册页面用户名输入框失去焦点后,异步请求后端查询用户名是否存在,存在则立即提示“该用户名已被注册”,不用等提交表单才反馈。这是前端体验的大提升,代码量只要一个Servlet加几行JavaScript。

第二个推荐验证码。登录页面加一个简单的图片验证码,把随机生成的字符串写进session,验证时比对。它能防止恶意暴力破解登录接口,也是很多系统的基础安全要求。

第三个推荐库存预警。后台药品列表页加一个字段,当库存低于某个阈值(比如10)时,用CSS把字体标红,或者单独做一个“库存预警列表”。这个功能虽然只是一条SQL加一个页面判断,但业务意义清晰,答辩时展示效果不错。

第四个推荐销量统计。在后台首页展示几个简单的统计数字:累计用户数、药品总数、订单总数、累计营业额。营业额本质就是对orders表里状态不为已取消的订单做SUM(total_price),一条SQL就能搞定。再要求高一点,可以用后台图表插件做柱状图展示最近七天的订单量走势,视觉冲击力很强。

关于时间和精力的分配,我个人在反复指导这类项目后最深的体会是:数据库设计和事务逻辑占整个项目价值的百分之五十,页面美化占百分之三十,配置调试占百分之二十。很多同学把时间全花在调页面样式上,想让界面花哨一些,却忽略了项目最核心的订单流程逻辑,答辩时被评委随口一问就露馅了——你界面再好看,事务回滚讲不清,分照样扣。

反过来,如果你能把“为什么订单明细要冗余药品名”“为什么扣库存要用库存大于0的UPDATE”“为什么购物车结算要放在一个事务里”这三句话讲清楚,这个毕业设计的技术深度就已经超过了绝大多数同期作品。所有技术方案的选择,都应该以“能讲清楚、能扛住追问”为出发点。

这个题目的上限比想象中高。从这节课出发,后续还可以继续把权限控制改成基于角色,把用户中心做成多地址管理,把支付环节接入第三方沙箱环境,甚至可以做一个小程序版本的服务端。先把手里的JSP版本老老实实跑通,理解每一层在干什么,那些“进阶升级”自然就是水到渠成的事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询