
简介《某商店进销存管理系统》课程设计报告是一份面向高校数据库或信息系统课程设计的完整参考文档适合在“进销存”选题中需要完成报告撰写、答辩展示或方案参考的学生。报告以某商店日常业务为背景系统梳理了商品分类管理、供应商记录、库存实时更新、销售与进货信息登记、报损原因记录以及管理员权限控制等需求后续章节依次展开分E-R图与全局E-R图、关系模式转换、物理结构设计、数据表创建及系统维护并配有进货、销售业务流程图与数据字典结构性和完整性较强。资源包共1个文件为docx格式大小约580KB可直接打开参考或编辑。当前已有118人学习下载。对希望快速建立课程设计框架、掌握数据库设计各阶段写法、并完善进销存系统细节的用户而言这份文档提供了可复用的模板和较成体系的示例内容。1. 进销存管理系统课程设计难的不是增删改查而是库存账如果只看到“某商店进销存管理系统”这几个字很容易把它当成一个普通的 CRUD 练习。但真正把课程设计报告做完整的人会发现进货单、销售单、库存报表三者要能对得上难点在“库存账”而不是页面。用 JSPServlet 做这个系统到今天仍不算过时因为它足够暴露每一步代码也方便把 JDBC 事务、DAO 分层、报表 SQL 都有理有据地写进报告里。这篇文章会从数据模型怎么建、Servlet 里事务怎么控、盘点调整怎么做一直讲到答辩被问“超卖怎么办”时该怎么答。适合正在赶课程设计的学生也适合需要带新人的工程师当一份脚手架参考。2. 进销存管理系统的数据模型先分清商品、库存和流水2.1 需求边界课程设计不是 ERP商店进销存听起来简单但需求蔓延起来可以没边。我一般会先把范围卡在三类角色和六件事上管理员维护商品资料、供应商资料收银员打销售单库管做进货入库、退货、盘点。报表由管理员查看。功能模块对应业务课程设计里常见的错误商品资料商品名称、条码、规格、进价、售价条码不唯一直接拿自增 ID 当条码进货入库供应商、入库单、入库明细只改库存表没有单据无法追溯销售出库销售单、收款金额允许自己输入价格导致毛利失真库存盘点实际数-账面数差异调整直接 UPDATE 库存表没有审批报表统计进销存汇总按日/月在 Java 内存里算不写 SQL数据量一大就卡边界想清楚之后页面数量基本就定了登录页、商品列表、进货单、销售单、盘点单、报表页。再往下加需求就是给自己挖坑。这里有个判断标准如果一张表不能回答“这个数字是怎么来的”就不应该放进课程设计。2.2 库存表与流水表分离为什么不能只在商品表里放库存字段最省事的做法是给 product 表加一列 stock销售时stock stock - 1。表面好看但课程设计报告到了“报表”环节就会露馅今天进了多少上个月某天的库存是多少回答不了。因为操作过程被覆盖了只剩一个结果。正确的做法是拆成两张表stock 只保存当前数量和最近更新时间stock_flow 保存每一笔库存变动。换句话说stock 是冗余的“读模型”stock_flow 才是事实来源。某个时刻的库存永远可以由流水的历史求和得到某日期结存 所有发生日期早于该日期的流水 quantity 之和这个思想写进报告比贴十个页面截图有用得多。它对应了会计上的“流水账”概念也是答辩时评委最愿意听的设计理由。2.3 建表 SQL 与字段参数小数用 DECIMAL时间用 DATETIME下面是一个能跑通核心流程的 MySQL 建表脚本按四张表起步supplier、product、stock、stock_flow。CREATE TABLE supplier ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 供应商ID, name VARCHAR(100) NOT NULL COMMENT 供应商名称, contact VARCHAR(50) DEFAULT NULL COMMENT 联系人, phone VARCHAR(20) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, barcode VARCHAR(30) DEFAULT NULL, name VARCHAR(100) NOT NULL COMMENT 商品名, spec VARCHAR(50) DEFAULT NULL COMMENT 规格, unit VARCHAR(10) NOT NULL DEFAULT 件, purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 参考进价, sale_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 售价, supplier_id INT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_supplier (supplier_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock ( product_id INT PRIMARY KEY, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_flow ( id INT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, flow_type TINYINT NOT NULL COMMENT 1进货 2销售 3退货 4盘点调整, quantity INT NOT NULL COMMENT 入库为正出库为负, ref_no VARCHAR(40) DEFAULT NULL COMMENT 业务单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_prod_time (product_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明quantity用 INT 而不是 VARCHAR因为业务是“件”但如果商店涉及散称商品数量要改成DECIMAL(10,3)所有涉及数量的表和 Java 类型同步改不要出现一个表 INT、一个表 DECIMAL 的错配。金额列一律DECIMAL(10,2)。float 和 double 在二进制下无法精确表示 0.1课程设计里经常因为这个问题导致报表差一分钱。stock_flow的flow_type只表示业务类型quantity统一带符号。这样SUM(quantity)可以直接算净变化量不用在 SQL 里为每种类型写不同的方向判断。外键没有写进 DDL只留下supplier_id、product_id做逻辑关联。原因JSP 项目多半需要手工导数据外键会导致先删父表、后插子表非常麻烦。但逻辑外键必须在 DAO 层校验存在性报告里要写清楚。提示字符集选 utf8mb4 而不是 utf8否则商品名里出现生僻字或 emoji 会插入失败。这个细节经常被忽略。2.4 单据主表与明细表销售数据不乱的前提光有商品和流水还不够。进货单、销售单需要体现“一张单包含多个商品”的结构所以要拆主表和明细表。以销售为例CREATE TABLE sale_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(30) UNIQUE COMMENT 销售单号, sale_date DATETIME NOT NULL, operator VARCHAR(30) COMMENT 操作人, total_amount DECIMAL(10,2) DEFAULT 0.00 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sale_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL COMMENT 正数代表销售数量, price DECIMAL(10,2) NOT NULL COMMENT 成交单价, amount DECIMAL(10,2) DEFAULT 0.00 COMMENT 行金额 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;主表sale_order.total_amount不要信任页面传的值应该在保存完成后用SUM(sale_item.amount)回写。这样即使前端被改报表口径仍然是数据库计算的。明细表quantity是正数与 stock_flow 中销售出库为负数形成区别这是两层模型的职责差异单据层讲业务语义流水层讲方向统一。报告里能用一句话讲清楚这个区别比堆功能列表更能体现设计能力。3. 用 jspservlet 实现进销存管理系统的分层与事务控制3.1 包结构把 Java 代码从 JSP 里赶出去JSP 里写% ... %虽然能跑但报告答辩时很难自圆其说。常见做法是严格分层JSP 只负责渲染Servlet 只接收请求和转发DAO 只操纵数据库。一个可周转的包结构如下src/com/store/ entity/Product.java entity/SaleOrder.java dao/ProductDao.java dao/StockFlowDao.java servlet/LoginServlet.java servlet/SaleServlet.java util/DBUtil.java WebContent/ WEB-INF/web.xml WEB-INF/pages/sale.jsp WEB-INF/pages/inventory.jsp login.jsp把页面放WEB-INF/pages下用户不能通过地址栏直接访问必须由 Servlet 转发后才能看到。这样登录拦截也简单web.xml里配一个 Filter 检查 session就成了一个雏形完整的权限系统。这个结构放进课程设计报告的“系统设计”章节说服力远大于截图。3.2 web.xml 映射与连接池参数Druid 的必调参数Servlet 3.1 可以用WebServlet(/sale)但课程设计我一般保留 web.xml。因为评审老师不一定熟悉注解web.xml 能直观表现出 URL 与类的映射关系。下面是一个最小配置web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-nameShopInventory/display-name servlet servlet-nameSaleServlet/servlet-name servlet-classcom.store.servlet.SaleServlet/servlet-class /servlet servlet-mapping servlet-nameSaleServlet/servlet-name url-pattern/sale/url-pattern /servlet-mapping welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-app这里url-pattern是/sale表单actionsale提交后请求先由容器交给SaleServletServlet 处理完再转发到WEB-INF/pages/sale.jsp。如果你用 Tomcat 9版本号写 3.1 没问题如果赶新鲜用 Tomcat 10则要用jakarta.servlet包名这一点不要忘记。数据库连接用 Druid 的话核心参数写在druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/shop_inventory?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait3000 minIdle3 validationQuerySELECT 1这几个参数在报告里建议逐个说明参数建议值作用initialSize5启动时预建 5 个连接避免第一个请求慢maxActive20最多 20 个连接课程设计单机演示 10~20 足够maxWait3000拿不到连接时最多等 3 秒超过即抛异常避免页面假死validationQuerySELECT 1探活连接是否有效MySQL 用 SELECT 1 开销最低连接池里最容易翻车的是serverTimezone不设Tomcat 连 MySQL 8 会直接报时区错误。另一个常见问题是characterEncodingutf8它必须出现在 JDBC URL 上只改 JSP 页面编码不会让写入数据库的中文正确。3.3 库存出库事务用条件 UPDATE 防超卖超卖是进销存系统的经典大坑。如果先SELECT quantity再在 Java 里判断够不够最后UPDATE两个并发请求可能同时读到 10 件都认为能卖结果库存变成负数。更稳的做法是把判断放进 UPDATE 的 WHERE 条件里。public boolean saleOut(int productId, int qty, String orderNo) { String updateStock UPDATE stock SET quantity quantity - ? WHERE product_id ? AND quantity ?; String insertFlow INSERT INTO stock_flow(product_id, flow_type, quantity, ref_no) VALUES(?, 2, ?, ?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateStock)) { ps1.setInt(1, qty); ps1.setInt(2, productId); ps1.setInt(3, qty); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; } try (PreparedStatement ps2 conn.prepareStatement(insertFlow)) { ps2.setInt(1, productId); ps2.setInt(2, -qty); ps2.setString(3, orderNo); ps2.executeUpdate(); } conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw e; } } catch (Exception e) { // 记录日志后返回业务失败 return false; } }逻辑说明UPDATE ... WHERE quantity ?是带条件扣减。如果当前库存小于 qty这一行影响行数为 0数据库会等待并返回 0不会把库存扣成负数。两个请求同时执行时行锁会让后一个请求等前一个提交后再判断天然避免超卖。INSERT stock_flow里的-qty是因为第 2 章约定“出库数量为负”。这里容易写错成qty写错后库存表正确但流水表方向反了报表会永远对不上。扣库存和插流水必须放在同一个连接、同一个事务里。二者缺一就会产生“有流水没库存”或“有库存没流水”的脏账。代码里省略了sale_order和sale_item的插入。实际开发中它们要在同一个事务内、UPDATE之前完成这样一张销售单的所有数据要么全部成功要么全部回滚。返回false后Servlet 层要友好提示“库存不足”不能直接抛出 500。用request.setAttribute(msg, 库存不足)再转发回销售页即可。这段代码可以直接抄到课程设计的“核心功能实现”小节。答辩时如果被问到“怎么确保不超卖”就指 WHERE 这一行。4. 进销存管理系统核心功能进货、销售、盘点与报表 SQL4.1 进货入库以供货商为维度做入库单进货流程比销售简单但同样要走“单据-明细-流水”三步。页面上选择供应商后再动态添加多个商品行提交到PurchaseServlet后先校验商品是否存在、数量是否为正数然后在一个事务里插入purchase_order、purchase_item、更新stock、写入stock_flow。入库单号可以用时间戳加随机数生成例如PO20250401120001避免用自增 ID 当单号。抽查当天进货数据可以用下面的 SQLSELECT DATE(create_time) AS biz_date, COUNT(DISTINCT ref_no) AS order_count, SUM(quantity) AS in_qty FROM stock_flow WHERE flow_type 1 AND create_time CURDATE() GROUP BY DATE(create_time);flow_type 1过滤进货行为quantity为正所以直接 SUM 得到入库总量。这个查询在报告里可以作为“进货统计”模块的查询样例同时验证事务是否真的写入了流水。4.2 销售出库成交价为什么不能是文本框最常见的错误是销售页面让收银员手输单价原因省事但后果是报表的销售金额不可信。正确做法是把product.sale_price带出只允许输入数量折扣另做一列。如果需要打折也要保留原价和成交价两个字段报告中说明“原价用于对账折扣价用于实际收款”。下面是按时间段汇总销售明细的 SQLSELECT p.name, SUM(si.quantity) AS sale_qty, SUM(si.amount) AS sale_amount FROM sale_item si JOIN product p ON p.id si.product_id JOIN sale_order so ON so.id si.order_id WHERE so.sale_date 2025-04-01 AND so.sale_date 2025-05-01 GROUP BY p.name;参数说明sale_date用 start和 end 1 day的半开区间比BETWEEN更安全。因为日期时间有“时分秒”BETWEEN 2025-04-01 AND 2025-04-30会漏掉 30 日 23:59:59 以后的数据。这个细节在答辩时提出来非常加分。同时给一个流水类型对照表方便理解业务flow_typequantity 方向对库存的影响进货入库1正增加销售出库2负减少客户退货3正增加盘点调整4正/负按实际方向4.3 盘点调整必须走“盘点单 审批”而不是直接改库存盘点要解决的是“账面数对不上实际数”。正确流程是库管先录入商品实际数量系统算出差异adjust_qty actual_qty - stock_qty生成一张状态为“待审核”的盘点单。管理员审核通过后才在一个事务里把差异写入stock_flow并更新stock。如果差异为负stock_flow.quantity写负数差异为正写正数。整个过程不能直接UPDATE stock来瞒天过海否则没法回答“为什么库存变了”的问题。课程设计里可以简化只有 admin 角色能访问盘点审核接口其他角色点击时 Servlet 判断 session 里的角色不是 admin 就转发到错误页。这个逻辑写一个 Filter 拦截admin/*路径即可不需要引入 Spring Security。4.4 进销存报表期初结存 本期入库 - 本期出库报表是进销存系统最终要交的账。下面这条 SQL 查询指定月份每个商品的期初、进货、销售和期末数量SELECT p.id, p.name AS product_name, IFNULL(begin_qty.balance, 0) AS begin_qty, IFNULL(in_qty.qty, 0) AS in_qty, IFNULL(out_qty.qty, 0) AS out_qty, IFNULL(begin_qty.balance, 0) IFNULL(in_qty.qty, 0) - IFNULL(out_qty.qty, 0) AS end_qty FROM product p LEFT JOIN ( SELECT product_id, SUM(quantity) AS balance FROM stock_flow WHERE create_time 2025-04-01 GROUP BY product_id ) begin_qty ON p.id begin_qty.product_id LEFT JOIN ( SELECT product_id, SUM(quantity) AS qty FROM stock_flow WHERE flow_type 1 AND create_time 2025-04-01 AND create_time 2025-05-01 GROUP BY product_id ) in_qty ON p.id in_qty.product_id LEFT JOIN ( SELECT product_id, -SUM(quantity) AS qty FROM stock_flow WHERE flow_type 2 AND create_time 2025-04-01 AND create_time 2025-05-01 GROUP BY product_id ) out_qty ON p.id out_qty.product_id;逻辑说明begin_qty.balance是截至到查询开始日期之前的所有流水求和不区分 flow_type因为第 2 章已经把所有流水定义为“正进负出”所以这里的 SUM 就是时点库存。in_qty只统计进货out_qty用-SUM(quantity)把负的出库转成正数便于阅读。如果系统里发生了退货、盘点期末数量还要额外加flow_type3和flow_type4的列否则期末不等于期初加入库减出库报表会平不上。实际项目中不要用字符串拼接日期应该用PreparedStatement.setTimestamp传入两个参数。这样既避免 SQL 注入也能让 MySQL 正确利用时间索引。5. 课程设计报告的写作结构与答辩准备5.1 报告里先画数据流流程再写代码课程设计报告不是代码注释的堆砌。我一般建议先画一页的数据流流程JSP 页面 - Servlet - DAO - MySQL标注出“销售出库”的请求路径。图不用画得多漂亮关键是让答辩老师一眼看到分层。图之后放核心代码每段代码不超过 20 行配一段“为什么这么写”的说明。报告的核心是证明你懂边界不是展示代码量。5.2 造测试数据让报表区间覆盖月初和月末报表演示最怕数据只有当天几条看不出期初库存效果。可以用一个初始化 Servlet 或者 MySQL 事件向 stock_flow 连续生成 90 天数据把商品固定 10 个每隔两天生成一次进货每天生成一次销售日期随机落在不同钟点。生成时注意 quantity 正负规则一致。有了跨月数据后报表期初和期末不再是 0答辩时也能演示“2 月结存 3 月进货 - 3 月销售 3 月结存”的闭合计算。5.3 答辩高频问题超卖、精度、期初把下面三个问题练熟基本能镇场提问回答要点两个用户同时买最后一件商品怎么保证不超卖UPDATE stock SET quantityquantity-1 WHERE id? AND quantity1配合事务回滚。金额为什么不用 doubledouble 是二进制近似DECIMAL(10,2) 是定点数金额报表差一分钱很难查。期初库存怎么算不做快照用流水表按 create_time 期初日期做 SUM(quantity)stock 表只是展示用冗余。答辩时如果被追问“盘点差异为什么不能直接改库存”可以回到第 4.3 节盘点单要留审批记录所有库存变化都可以从 stock_flow 溯源。回答能用一句话落到表名和字段比背概念可信得多。答辩前再用 stock_flow 按商品 SUM(quantity) 与 stock.quantity 做 LEFT JOIN 比对不一致的就是事务漏提交这也是最值得提前修掉的隐藏 bug。本文还有配套的精品资源点击获取