简介:这是一套基于Java Web的超市管理系统完整项目资料,面向计算机相关专业的在校学生、课程设计或毕业设计需求者,以及希望以真实项目练手的学习者。项目已通过导师指导与答辩评审,获得95分评价,代码经过测试可正常运行,适合直接用于数据库课程设计、作业提交或项目初期立项演示。压缩包共125个文件,约3.05MB,以30个Java源文件、20个JSP页面、23个JavaScript脚本为核心,辅以XML配置、CSS样式、图片资源及properties配置,并包含war包与说明文档,结构完整、层次清晰。目前已有40人学习关注。读者可获得一套可运行的超市管理业务源码,涵盖商品、供应商等模块的前后端实现,便于理解Java Web分层架构与数据库交互逻辑;同时可参考其目录组织与配置方式,在现有基础上修改扩展功能,或作为课程设计、毕业设计的参考模板,降低从零搭建的成本。
1. 超市管理系统课设:从建表到答辩,一套能跑通的 Java Web 方案
做过数据库课程设计的人都清楚,最磨人的不是写代码,而是把需求、E-R 图、建表脚本、后端接口、前端页面和答辩文档串成一条线。这套基于 Java Web 的超市管理系统,核心就是围绕商品、库存、订单、会员、员工这几张表,把数据库增删改查完整落地成一个能演示的 Web 应用。它适合正在做数据库课程设计的学生,也适合想用一个小型项目把 JDBC、Servlet、MySQL 串起来练手的开发者。我见过太多人卡在“表建好了但接口对不上”或者“功能能跑但文档写不出来”这两步,所以这篇笔记按实际交付顺序来拆:先定表结构,再搭后端,然后接前端,最后补文档和答辩要点。整套方案不依赖复杂框架,用最朴素的 Servlet + JSP + MySQL 就能跑通,重点是让你在有限时间内交出一个逻辑自洽、演示不翻车的课设。
2. 数据库设计先定死:超市管理系统的表结构与字段取舍
2.1 五张核心表怎么划:商品、库存、订单、会员、员工
超市管理系统的数据库设计,最怕一上来就追求大而全。我一般会先把业务闭环画出来:员工登录系统 → 商品录入并绑定分类 → 库存随入库出库变动 → 会员下单生成订单 → 订单明细扣减库存。围绕这个闭环,五张表足够撑起一个课设的体量。
商品表product是主表,字段包括商品编号、名称、分类、进价、售价、单位、状态。这里有个取舍:分类到底用单独的分类表还是直接存字符串?如果课设要求体现范式,就拆一张category表,用外键关联;如果时间紧,直接在product里存分类名也能跑,但答辩时容易被问“这算第几范式”。我的建议是拆出来,多写一个下拉框查询而已,但 E-R 图会好看很多。
库存表inventory单独建,而不是把库存数量塞进商品表。原因是库存变动需要记录流水,入库和出库要留痕。表里放商品 ID、当前数量、预警阈值、最后更新时间。订单表orders和订单明细表order_item必须分开,这是数据库课设的经典考点:一对多关系。orders存订单号、会员 ID、员工 ID、总金额、下单时间、状态;order_item存订单号、商品 ID、数量、单价。会员表member和员工表employee分开,字段差异大,硬合在一起反而别扭。
提示:表名不要用
order,它是 SQL 关键字,MySQL 里虽然可以用反引号,但 JDBC 拼接时容易翻车,直接用orders省心。
2.2 建表 SQL 与索引:主键、外键、唯一约束一次写对
下面这段 SQL 是我在课设里反复用过的建表脚本,字段类型和约束都按 MySQL 8.0 来写。注意字符集用utf8mb4,否则商品名里的生僻字或表情符号会报错。
CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; -- 商品分类表 CREATE TABLE category ( cid INT PRIMARY KEY AUTO_INCREMENT, cname VARCHAR(50) NOT NULL UNIQUE ) ENGINE=InnoDB; -- 商品表 CREATE TABLE product ( pid INT PRIMARY KEY AUTO_INCREMENT, pname VARCHAR(100) NOT NULL, cid INT NOT NULL, purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0.00, sale_price DECIMAL(10,2) NOT NULL DEFAULT 0.00, unit VARCHAR(10) DEFAULT '件', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', CONSTRAINT fk_product_category FOREIGN KEY (cid) REFERENCES category(cid) ) ENGINE=InnoDB; -- 库存表 CREATE TABLE inventory ( iid INT PRIMARY KEY AUTO_INCREMENT, pid INT NOT NULL UNIQUE, quantity INT NOT NULL DEFAULT 0, warn_threshold INT DEFAULT 10, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_inventory_product FOREIGN KEY (pid) REFERENCES product(pid) ) ENGINE=InnoDB; -- 会员表 CREATE TABLE member ( mid INT PRIMARY KEY AUTO_INCREMENT, mname VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE, points INT DEFAULT 0, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 员工表 CREATE TABLE employee ( eid INT PRIMARY KEY AUTO_INCREMENT, ename VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT 'cashier' ) ENGINE=InnoDB; -- 订单表 CREATE TABLE orders ( oid INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, mid INT, eid INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT '1已完成 0已取消', CONSTRAINT fk_orders_member FOREIGN KEY (mid) REFERENCES member(mid), CONSTRAINT fk_orders_employee FOREIGN KEY (eid) REFERENCES employee(eid) ) ENGINE=InnoDB; -- 订单明细表 CREATE TABLE order_item ( oiid INT PRIMARY KEY AUTO_INCREMENT, oid INT NOT NULL, pid INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10,2) NOT NULL, CONSTRAINT fk_item_orders FOREIGN KEY (oid) REFERENCES orders(oid), CONSTRAINT fk_item_product FOREIGN KEY (pid) REFERENCES product(pid) ) ENGINE=InnoDB; -- 常用索引 CREATE INDEX idx_product_cid ON product(cid); CREATE INDEX idx_orders_time ON orders(order_time); CREATE INDEX idx_item_oid ON order_item(oid);这段脚本的逻辑说明:category和product是一对多,product和inventory是一对一,orders和order_item是一对多。外键约束保证不会出现孤儿记录,但要注意插入顺序——先插分类,再插商品,再插库存。索引建在product.cid、orders.order_time、order_item.oid上,因为后台按分类筛选商品、按时间查订单、按订单查明细是最频繁的操作。
参数说明:DECIMAL(10,2)表示总共 10 位,小数 2 位,金额字段不要用FLOAT,否则对账时会出现0.30000000000000004这种玄学数字。status用TINYINT而不是BOOLEAN,MySQL 里BOOLEAN本质也是TINYINT(1),但显式写TINYINT更直观。ON UPDATE CURRENT_TIMESTAMP让库存表在更新数量时自动刷新时间,省去手动维护。
2.3 初始化数据与测试用例:让演示不空屏
建完表直接跑系统,页面全是空的,演示效果很差。我一般会准备一份初始化数据脚本,至少插 3 个分类、10 个商品、5 个会员、2 个员工、3 条订单。
INSERT INTO category (cname) VALUES ('饮料'), ('零食'), ('日用品'); INSERT INTO product (pname, cid, purchase_price, sale_price, unit) VALUES ('可乐 330ml', 1, 2.00, 3.50, '瓶'), ('薯片 原味', 2, 4.50, 7.00, '袋'), ('抽纸 3层', 3, 6.00, 9.90, '提'); INSERT INTO inventory (pid, quantity, warn_threshold) VALUES (1, 100, 20), (2, 50, 10), (3, 30, 5); INSERT INTO member (mname, phone, points) VALUES ('张三', '13800000001', 120), ('李四', '13800000002', 80); INSERT INTO employee (ename, username, password, role) VALUES ('管理员', 'admin', '123456', 'admin'), ('收银员小王', 'wang', '123456', 'cashier');测试用例要覆盖:正常下单扣库存、库存不足时提示、会员积分累加、按分类查商品。这些用例直接写进课设文档的“测试章节”,答辩时老师问“你怎么验证的”,你就有东西说。
3. 后端接口落地:Servlet + JDBC 把增删改查串起来
3.1 数据库连接池怎么配:别再用 DriverManager 裸连
课设里最常见的翻车点就是每个 Servlet 里写一遍DriverManager.getConnection(),演示时点几下就报Too many connections。正确做法是用连接池。Tomcat 自带 DBCP,但配置略繁琐;我一般直接引HikariCP或者用Druid。如果课设不允许引额外 jar,就用 Tomcat 的context.xml配 JNDI 数据源。
下面以 HikariCP 为例,写一个工具类DBUtil:
import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("your_password"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception e) { /* 忽略 */ } } } } }逻辑说明:静态代码块在类加载时初始化连接池,整个应用生命周期只执行一次。maximumPoolSize设 10 足够课设演示,connectionTimeout设 30 秒,避免数据库没启动时线程一直挂起。close方法用可变参数统一关闭Connection、PreparedStatement、ResultSet,减少重复代码。
参数说明:serverTimezone=Asia/Shanghai必须加,否则 MySQL 8.0 的DATETIME字段取出来会差 8 小时,订单时间全乱。useSSL=false在本地演示时关掉,避免证书警告。characterEncoding=utf8配合数据库的utf8mb4,防止中文乱码。
3.2 商品模块的增删改查:一个 Servlet 写透
商品管理是课设里最核心的模块,包含列表查询、新增、修改、删除、按分类筛选。我一般用一个ProductServlet配合action参数来分发,而不是写五个 Servlet。
@WebServlet("/product") public class ProductServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if (action == null) action = "list"; switch (action) { case "list": list(req, resp); break; case "delete": delete(req, resp); break; case "edit": edit(req, resp); break; default: list(req, resp); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if ("add".equals(action)) add(req, resp); else if ("update".equals(action)) update(req, resp); } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cid = req.getParameter("cid"); String sql = "SELECT p.pid, p.pname, c.cname, p.purchase_price, p.sale_price, p.unit, p.status, i.quantity " + "FROM product p LEFT JOIN category c ON p.cid = c.cid " + "LEFT JOIN inventory i ON p.pid = i.pid"; if (cid != null && !cid.isEmpty()) { sql += " WHERE p.cid = ?"; } sql += " ORDER BY p.pid DESC"; List<ProductVO> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (cid != null && !cid.isEmpty()) { ps.setInt(1, Integer.parseInt(cid)); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ProductVO vo = new ProductVO(); vo.setPid(rs.getInt("pid")); vo.setPname(rs.getString("pname")); vo.setCname(rs.getString("cname")); vo.setPurchasePrice(rs.getBigDecimal("purchase_price")); vo.setSalePrice(rs.getBigDecimal("sale_price")); vo.setUnit(rs.getString("unit")); vo.setStatus(rs.getInt("status")); vo.setQuantity(rs.getInt("quantity")); list.add(vo); } } } catch (SQLException e) { throw new ServletException("查询商品失败", e); } req.setAttribute("products", list); req.getRequestDispatcher("/product_list.jsp").forward(req, resp); } private void add(HttpServletRequest req, HttpServletResponse resp) throws IOException { String pname = req.getParameter("pname"); int cid = Integer.parseInt(req.getParameter("cid")); BigDecimal purchasePrice = new BigDecimal(req.getParameter("purchasePrice")); BigDecimal salePrice = new BigDecimal(req.getParameter("salePrice")); String unit = req.getParameter("unit"); int quantity = Integer.parseInt(req.getParameter("quantity")); String sqlProduct = "INSERT INTO product (pname, cid, purchase_price, sale_price, unit) VALUES (?, ?, ?, ?, ?)"; String sqlInventory = "INSERT INTO inventory (pid, quantity) VALUES (?, ?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(sqlProduct, Statement.RETURN_GENERATED_KEYS); PreparedStatement ps2 = conn.prepareStatement(sqlInventory)) { ps1.setString(1, pname); ps1.setInt(2, cid); ps1.setBigDecimal(3, purchasePrice); ps1.setBigDecimal(4, salePrice); ps1.setString(5, unit); ps1.executeUpdate(); int pid; try (ResultSet rs = ps1.getGeneratedKeys()) { rs.next(); pid = rs.getInt(1); } ps2.setInt(1, pid); ps2.setInt(2, quantity); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { throw new RuntimeException("新增商品失败", e); } resp.sendRedirect("product?action=list"); } private void delete(HttpServletRequest req, HttpServletResponse resp) throws IOException { int pid = Integer.parseInt(req.getParameter("pid")); try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement("DELETE FROM inventory WHERE pid = ?"); PreparedStatement ps2 = conn.prepareStatement("DELETE FROM product WHERE pid = ?")) { ps1.setInt(1, pid); ps1.executeUpdate(); ps2.setInt(1, pid); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { throw new RuntimeException("删除商品失败", e); } resp.sendRedirect("product?action=list"); } private void edit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pid = Integer.parseInt(req.getParameter("pid")); String sql = "SELECT * FROM product WHERE pid = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, pid); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { req.setAttribute("product", rs); req.getRequestDispatcher("/product_edit.jsp").forward(req, resp); } } } catch (SQLException e) { throw new ServletException("查询商品详情失败", e); } } private void update(HttpServletRequest req, HttpServletResponse resp) throws IOException { int pid = Integer.parseInt(req.getParameter("pid")); String pname = req.getParameter("pname"); int cid = Integer.parseInt(req.getParameter("cid")); BigDecimal purchasePrice = new BigDecimal(req.getParameter("purchasePrice")); BigDecimal salePrice = new BigDecimal(req.getParameter("salePrice")); String unit = req.getParameter("unit"); String sql = "UPDATE product SET pname = ?, cid = ?, purchase_price = ?, sale_price = ?, unit = ? WHERE pid = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, pname); ps.setInt(2, cid); ps.setBigDecimal(3, purchasePrice); ps.setBigDecimal(4, salePrice); ps.setString(5, unit); ps.setInt(6, pid); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("更新商品失败", e); } resp.sendRedirect("product?action=list"); } }逻辑说明:doGet处理查询和删除,doPost处理新增和修改,这是 HTTP 语义的常见做法。list方法用LEFT JOIN把分类名和库存数量一起查出来,避免在 JSP 里再查一次数据库。add方法用事务保证商品和库存同时插入,Statement.RETURN_GENERATED_KEYS拿到自增主键,再插入库存表。delete方法先删库存再删商品,因为有外键约束,顺序反了会报错。
参数说明:req.setCharacterEncoding("UTF-8")必须放在doPost第一行,否则表单里的中文会乱码。conn.setAutoCommit(false)开启事务,conn.commit()提交,conn.rollback()回滚。BigDecimal接收金额参数,避免浮点精度问题。
3.3 订单与库存联动:一个事务里扣减库存
订单模块是课设的加分项,也是最能体现数据库能力的地方。下单时要同时做三件事:插入订单主表、插入订单明细、扣减库存。这三步必须在一个事务里,否则会出现“订单生成了但库存没扣”的脏数据。
public void createOrder(int mid, int eid, List<CartItem> items) throws SQLException { String sqlOrder = "INSERT INTO orders (order_no, mid, eid, total_amount) VALUES (?, ?, ?, ?)"; String sqlItem = "INSERT INTO order_item (oid, pid, quantity, unit_price) VALUES (?, ?, ?, ?)"; String sqlStock = "UPDATE inventory SET quantity = quantity - ? WHERE pid = ? AND quantity >= ?"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try { BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { total = total.add(item.getUnitPrice().multiply(new BigDecimal(item.getQuantity()))); } int oid; try (PreparedStatement ps = conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, generateOrderNo()); ps.setInt(2, mid); ps.setInt(3, eid); ps.setBigDecimal(4, total); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { rs.next(); oid = rs.getInt(1); } } try (PreparedStatement psItem = conn.prepareStatement(sqlItem); PreparedStatement psStock = conn.prepareStatement(sqlStock)) { for (CartItem item : items) { psItem.setInt(1, oid); psItem.setInt(2, item.getPid()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getUnitPrice()); psItem.addBatch(); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getPid()); psStock.setInt(3, item.getQuantity()); if (psStock.executeUpdate() == 0) { throw new SQLException("库存不足,商品ID:" + item.getPid()); } } psItem.executeBatch(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } }逻辑说明:sqlStock的WHERE quantity >= ?是关键,它保证库存不足时更新影响行数为 0,直接抛异常触发回滚。psItem.addBatch()和executeBatch()批量插入明细,减少数据库往返。订单号用时间戳加随机数生成,保证唯一。
参数说明:generateOrderNo()可以用System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000))。CartItem是自定义的购物车项类,包含商品 ID、数量、单价。事务隔离级别用默认的REPEATABLE READ即可,课设场景不需要调整。
4. 前端页面与文档:让演示流畅、答辩有料
4.1 JSP 页面怎么写:列表、表单、分页三件套
课设的前端不需要多漂亮,但必须能跑通。我一般用 JSP + JSTL 写三个页面:product_list.jsp、product_edit.jsp、order_list.jsp。列表页用<c:forEach>遍历后端传来的products,表单页用<form>提交到product?action=add或update。
分页是课设的常见要求,但很多人写得很复杂。我的做法是在 SQL 里用LIMIT和OFFSET,后端算好总页数,前端只渲染上一页、下一页和页码。
SELECT p.pid, p.pname, c.cname, p.sale_price, i.quantity FROM product p LEFT JOIN category c ON p.cid = c.cid LEFT JOIN inventory i ON p.pid = i.pid ORDER BY p.pid DESC LIMIT ? OFFSET ?;参数说明:LIMIT是每页条数,OFFSET是(当前页码 - 1) * 每页条数。总条数用SELECT COUNT(*)单独查一次。注意LIMIT和OFFSET在 MySQL 里不能用?占位符直接传,需要用PreparedStatement.setInt()设置。
注意:JSP 里不要写 Java 代码块
<% %>,用 JSTL 和 EL 表达式,否则答辩时老师会问“你这算 MVC 吗”。
4.2 课设文档结构:从需求到测试的完整链条
文档是课设的另一半分数。我一般按这个结构写:封面、目录、需求分析、概念设计(E-R 图)、逻辑设计(表结构)、物理设计(建表 SQL)、系统实现(关键代码截图)、测试(测试用例表)、总结与展望。E-R 图用 draw.io 画,导出 PNG 插入文档。表结构用表格列出字段名、类型、约束、说明。
测试章节要写具体:测试功能、输入数据、预期结果、实际结果、是否通过。比如“库存不足时下单”,输入数量 999,预期提示“库存不足”,实际提示一致,通过。这样答辩时老师翻到这一页,基本不会为难你。
4.3 答辩演示脚本:三分钟讲清一个闭环
答辩演示不要从头到尾点菜单,而是讲一个完整故事:登录 → 新增商品 → 查看库存 → 会员下单 → 库存扣减 → 订单查询。每一步说清楚“我点了什么、系统做了什么、数据库哪张表变了”。提前准备好数据,避免现场录入浪费时间。如果老师问“并发下单会不会超卖”,你就答“我在库存扣减的 SQL 里加了quantity >= ?条件,并且用事务保证原子性”,这是加分项。
5. 避坑与排查:课设里最容易翻车的五个地方
5.1 中文乱码:从数据库到 JSP 的完整链路
现象:商品名在数据库里正常,页面上显示???。原因:数据库字符集、连接 URL、JSP 页面编码、Servlet 请求编码四处不一致。解决:数据库用utf8mb4,JDBC URL 加characterEncoding=utf8,JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>,doPost第一行加req.setCharacterEncoding("UTF-8")。四处都对了,乱码基本消失。
5.2 外键约束报错:插入顺序和删除顺序
现象:新增商品时报Cannot add or update a child row。原因:product.cid引用了category.cid,但插入时分类不存在。解决:先插分类,再插商品。删除时反过来,先删库存和订单明细,再删商品和订单主表。如果课设不要求严格外键,可以在建表时去掉FOREIGN KEY,但答辩时会被问“你怎么保证数据一致性”,所以建议保留。
5.3 连接池耗尽:Too many connections
现象:演示时点几下就报Too many connections。原因:每次请求都DriverManager.getConnection(),没有关闭连接。解决:用连接池,并且在finally块里关闭Connection、PreparedStatement、ResultSet。如果用了try-with-resources,就不需要手动关。检查代码里有没有漏关的地方,尤其是ResultSet。
5.4 事务不回滚:库存扣了但订单没生成
现象:下单后库存减少了,但订单列表里没有记录。原因:conn.setAutoCommit(false)之后没有commit(),或者异常被吞了没有rollback()。解决:把commit()放在try块最后,rollback()放在catch块里。确保异常往上抛,不要catch了什么都不做。
5.5 时间差 8 小时:serverTimezone 没配
现象:订单时间比实际时间早 8 小时。原因:MySQL 8.0 默认时区是 UTC,JDBC 没指定时区。解决:JDBC URL 加serverTimezone=Asia/Shanghai。如果已经建了表,DATETIME字段的历史数据不会自动修正,需要手动UPDATE或重新插入。
6. 进阶技巧:把课设变成能写进简历的项目
课设做完只是及格,想拿高分或者写进简历,还得加一点进阶内容。我一般会从三个方向挑一个:加 Redis 缓存商品列表、加拦截器做登录校验、加日志记录关键操作。这里说一个最实用的:用过滤器做统一编码和登录校验。
@WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String uri = request.getRequestURI(); if (uri.endsWith("login.jsp") || uri.endsWith("login") || uri.contains("css") || uri.contains("js")) { chain.doFilter(req, resp); return; } Object employee = request.getSession().getAttribute("employee"); if (employee == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }逻辑说明:过滤器拦截所有请求,先设置编码,再判断是否是登录页或静态资源,最后检查 session 里有没有员工信息。没有就跳转登录页。这样每个 Servlet 里不用重复写编码和登录校验。
参数说明:@WebFilter("/*")拦截所有路径,uri.contains("css")放行静态资源。request.getContextPath()拿到项目上下文路径,避免跳转时路径错误。
验证方法:启动 Tomcat,直接访问product?action=list,如果没登录应该跳转到login.jsp;登录后再访问,应该正常显示列表。如果过滤器不生效,检查web.xml的版本是否支持注解,或者@WebFilter是否被扫描到。
我自己的习惯是,课设做完后把代码传到 GitHub,README 里写清楚技术栈、功能列表、启动步骤和截图。面试时直接打开仓库,比说“我做过一个超市管理系统”有说服力得多。数据库课设的价值不在于系统多复杂,而在于你能不能把需求、设计、实现、测试这条链路讲清楚。希望帮到你。
本文还有配套的精品资源,点击获取