拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP+MSSQL进销存毕设源码拆解:数据模型、JSP链路与避坑指南

JSP+MSSQL进销存毕设源码拆解:数据模型、JSP链路与避坑指南 简介这份资源是面向Java Web初学者与毕业设计学生的进销存管理系统完整项目包采用JSP作为表现层、MSSQL作为数据库配合Servlet与JavaBean实现三层架构可用于课程设计参考、毕设选题落地或Java Web入门练手。压缩包共163个文件约1.7MB以105个class编译文件与31个java源码为主另含4个jar依赖、8张png界面截图、mdf与ldf数据库文件及1份doc毕设文档覆盖商品、供应商、进货、销售、库存与报表分析等模块并附带需求分析、系统设计、测试评价等文档内容。目前已有248人学习下载。通过阅读源码与文档读者可理清JSP页面请求如何与后端逻辑关联、JDBC如何操作MSSQL掌握进销存业务的数据流转与表结构设计适合作为理解Java Web开发流程与数据库交互的实践素材。1. 从一份毕设.zip说起JSPMSSQL进销存到底能不能打每年毕业季总有一批计算机专业的学生在深夜对着一个叫“java进销存管理系统的设计与实现jspmssql包含源代码毕设.zip”的压缩包发愁。打开一看里面是几十个JSP文件、一堆JavaBean、一个MSSQL的.bak备份文件还有一份Word格式的论文初稿。这个场景太典型了——它既是很多人的毕业救命稻草也是不少人工作后回头看觉得“这代码写得真糙”的起点。但我想说的是别急着嫌弃它。JSPMSSQL这套组合虽然老却恰好覆盖了进销存系统最核心的业务闭环采购入库、销售出库、库存盘点、往来账目。对于想理解“一个业务系统怎么从数据库表设计一路做到页面交互”的人来说这套技术栈反而比Spring BootMyBatisRedis那套更容易看清全貌因为JSP把请求响应周期暴露得足够直白MSSQL的存储过程和触发器也能让你把业务逻辑直接焊在数据库层。这篇文章不打算复述那份论文里的需求分析而是把这份毕设.zip拆开讲清楚三件事这套系统的数据模型为什么这么设计、JSP里那些看起来啰嗦的代码到底在防什么、以及如果你要把它跑起来或者改造成能用的东西哪些参数和配置是必须动的。适合手里正拿着类似源码、想把它跑通或者想拿它当练手项目的人。2. 进销存的数据模型从三张核心表到MSSQL的约束设计2.1 商品、库存、单据为什么大部分毕设都从这三张表开始打开任何一份进销存源码不管前端是JSP还是Vue数据库里一定有三张绕不开的表商品信息表、库存表、出入库单据表。这不是偷懒而是业务本身决定的。商品表定义“有什么”库存表记录“有多少”单据表回答“为什么变”。很多同学一上来就想设计得很复杂搞出商品分类表、品牌表、供应商表、客户表、仓库表、批次表结果做到一半发现连最基本的“查某个商品当前库存”都要关联五六张表JSP页面里写个查询能写两百行。我的建议是第一版只保留三张核心表把外键关系理清楚等跑通了再按需扩展。以MSSQL为例商品表通常长这样CREATE TABLE Product ( ProductID INT IDENTITY(1,1) PRIMARY KEY, ProductCode VARCHAR(50) NOT NULL UNIQUE, ProductName NVARCHAR(100) NOT NULL, Unit VARCHAR(20) DEFAULT 个, PurchasePrice DECIMAL(18,2) DEFAULT 0, SalePrice DECIMAL(18,2) DEFAULT 0, CreateTime DATETIME DEFAULT GETDATE() );这里有几个参数值得注意。ProductCode加了UNIQUE约束因为进销存里商品编码重复是灾难性的后期对账根本对不上。金额字段用DECIMAL(18,2)而不是FLOAT浮点数在累加时会出现0.10.2≠0.3的经典问题库存金额算着算着就差几分钱。CreateTime默认GETDATE()省去了Java层手动赋值少写一行代码就少一个忘记赋值的可能。库存表的设计更微妙。很多毕设版本直接在商品表里加一个StockQuantity字段简单粗暴。但这样做的问题是你无法追溯库存变化的历史。正确的做法是单独建一张库存表并且只记录当前结存CREATE TABLE Inventory ( InventoryID INT IDENTITY(1,1) PRIMARY KEY, ProductID INT NOT NULL, WarehouseID INT DEFAULT 1, Quantity INT DEFAULT 0, LastUpdate DATETIME DEFAULT GETDATE(), CONSTRAINT FK_Inventory_Product FOREIGN KEY (ProductID) REFERENCES Product(ProductID) );Quantity字段用INT而不是DECIMAL因为大多数进销存场景下商品按整数管理。如果你的业务涉及称重商品再改成DECIMAL(18,3)。外键约束是必须加的JSP层可以不做校验但数据库层一定要拦住非法的ProductID插入否则后期会出现“库存表里有记录但商品表里找不到对应商品”的脏数据。单据表是整个系统的灵魂。采购单、销售单、退货单本质上都是“某个商品在某个时间发生了数量变化”。所以设计一张统一的StockBill表比分开建三张表更实用CREATE TABLE StockBill ( BillID INT IDENTITY(1,1) PRIMARY KEY, BillType CHAR(2) NOT NULL, -- IN入库 OUT出库 ProductID INT NOT NULL, Quantity INT NOT NULL, BillDate DATETIME DEFAULT GETDATE(), Operator VARCHAR(50), Remark NVARCHAR(200), CONSTRAINT FK_Bill_Product FOREIGN KEY (ProductID) REFERENCES Product(ProductID) );BillType用CHAR(2)存IN和OUT比用TINYINT存1和2更直观JSP页面里判断也方便。Quantity永远存正数方向由BillType决定这样统计“某商品总入库量”时直接SUM(Quantity) WHERE BillTypeIN就行不用考虑正负号。2.2 在MSSQL里写触发器还是写Java库存更新的两种落地路径库存更新是进销存系统里最容易出bug的地方。假设销售出库时JSP页面提交了一张出库单后台需要做两件事往StockBill表插一条记录同时把Inventory表的Quantity减掉。这两步必须同时成功或同时失败否则就会出现“单据有了但库存没减”或者“库存减了但单据没插进去”的灵异事件。常见做法有两种。第一种是在Java层用JDBC事务控制Connection conn null; try { conn DriverManager.getConnection(url, user, password); conn.setAutoCommit(false); // 插入单据 String sql1 INSERT INTO StockBill(BillType,ProductID,Quantity,Operator) VALUES(?,?,?,?); PreparedStatement ps1 conn.prepareStatement(sql1); ps1.setString(1, OUT); ps1.setInt(2, productId); ps1.setInt(3, quantity); ps1.setString(4, operator); ps1.executeUpdate(); // 更新库存 String sql2 UPDATE Inventory SET Quantity Quantity - ? WHERE ProductID ?; PreparedStatement ps2 conn.prepareStatement(sql2); ps2.setInt(1, quantity); ps2.setInt(2, productId); int rows ps2.executeUpdate(); if (rows 0) { throw new SQLException(库存记录不存在); } conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) conn.close(); }这段代码的关键在于setAutoCommit(false)和commit/rollback的配对。很多毕设里忘记写rollback结果出错后连接归还到连接池时还带着未提交的事务下一个请求拿到这个连接就会看到脏数据。另外注意rows 0的判断如果Inventory表里根本没有这个商品的记录UPDATE会返回0行这时候必须抛异常回滚否则单据插进去了但库存没动。第二种做法是在MSSQL里写触发器CREATE TRIGGER trg_StockBill_Insert ON StockBill AFTER INSERT AS BEGIN SET NOCOUNT ON; UPDATE Inventory SET Quantity CASE WHEN i.BillType IN THEN Inventory.Quantity i.Quantity WHEN i.BillType OUT THEN Inventory.Quantity - i.Quantity END, LastUpdate GETDATE() FROM Inventory INNER JOIN inserted i ON Inventory.ProductID i.ProductID; END触发器的好处是Java层只需要插单据库存自动更新代码量少。坏处是逻辑藏在数据库里调试时容易忘记它的存在而且批量插入时触发器逐行执行性能会下降。我的建议是如果团队里有人熟悉MSSQL用触发器如果都是Java开发为主老老实实在Service层用事务控制。两种方式不要混用否则库存会被减两次。3. JSP页面里的门道从表单提交到分页查询的完整链路3.1 一个入库单页面到底要写哪些JSP标签打开毕设里的入库单页面通常叫stock_in.jsp或者类似的名字。新手看这种页面会觉得乱因为JSP把Java代码、HTML、JavaScript混在一起。但如果你按请求生命周期去读其实很清晰。页面顶部一般是表单用来输入商品编码、数量、供应商中间是提交按钮底部是最近入库记录列表。表单部分的核心是这段form actionStockServlet?actionadd methodpost 商品编码input typetext nameproductCode idproductCode onblurcheckProduct()/ span idproductName/spanbr/ 入库数量input typetext namequantity /br/ 供应商input typetext namesupplier /br/ input typesubmit value提交入库 / /form注意οnblurcheckProduct()这个事件。进销存系统里操作员输入商品编码后页面应该立刻显示商品名称防止输错编码。这个校验通常用AJAX实现function checkProduct() { var code document.getElementById(productCode).value; var xhr new XMLHttpRequest(); xhr.open(GET, StockServlet?actioncheckProductcode encodeURIComponent(code), true); xhr.onreadystatechange function() { if (xhr.readyState 4 xhr.status 200) { var resp xhr.responseText; if (resp NOT_FOUND) { document.getElementById(productName).innerHTML font colorred商品不存在/font; } else { document.getElementById(productName).innerHTML resp; } } }; xhr.send(); }这里用原生XMLHttpRequest而不是jQuery因为毕设环境里不一定有jQuery库。encodeURIComponent是必须的商品编码里如果包含特殊字符比如不编码会导致参数截断。后端StockServlet里对应处理actioncheckProduct的分支查Product表返回商品名称或NOT_FOUND。提交后的处理在Servlet里protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (add.equals(action)) { String productCode request.getParameter(productCode); int quantity Integer.parseInt(request.getParameter(quantity)); String supplier request.getParameter(supplier); // 根据编码查商品ID int productId productDao.getProductIdByCode(productCode); if (productId -1) { request.setAttribute(error, 商品编码不存在); request.getRequestDispatcher(stock_in.jsp).forward(request, response); return; } // 调用Service层入库 stockService.stockIn(productId, quantity, supplier); response.sendRedirect(stock_in.jsp?msgsuccess); } }Integer.parseInt这里没有做异常捕获如果用户输入了非数字会直接抛NumberFormatException页面显示500错误。更友好的做法是用try-catch包住给用户提示“数量必须是数字”。另外sendRedirect后面的代码不会执行所以不用写return但加上return是个好习惯防止后续维护时在重定向后面又加了代码导致逻辑混乱。3.2 分页查询为什么LIMIT在MSSQL里要写成ROW_NUMBERJSP进销存系统里库存列表、单据列表都需要分页。MySQL里用LIMIT offset, size很简单但MSSQL尤其是2005和2008版本不支持LIMIT必须用ROW_NUMBER() OVER()。这是很多从MySQL转过来的同学最容易翻车的地方。假设每页显示20条当前是第3页MSSQL的写法是SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY BillID DESC) AS RowNum, * FROM StockBill WHERE BillType IN ) AS T WHERE T.RowNum BETWEEN 41 AND 60BETWEEN 41 AND 60对应第3页每页20条前两页是1-40。ROW_NUMBER()里的ORDER BY必须和业务排序一致否则分页会出现重复或遗漏。比如你按BillID降序排但ROW_NUMBER()里忘了写ORDER BYMSSQL会报错如果写了ORDER BY BillDate但BillDate有重复值分页时同一页可能出现两条相同记录。对应的JSP分页控件% int pageSize 20; int currentPage 1; String pageStr request.getParameter(page); if (pageStr ! null) { currentPage Integer.parseInt(pageStr); } int totalCount (Integer) request.getAttribute(totalCount); int totalPages (totalCount pageSize - 1) / pageSize; % div 共 %totalCount% 条记录第 %currentPage%/%totalPages% 页 % if (currentPage 1) { % a hrefstock_list.jsp?page%currentPage-1%上一页/a % } % % if (currentPage totalPages) { % a hrefstock_list.jsp?page%currentPage1%下一页/a % } % /divtotalPages的计算用(totalCount pageSize - 1) / pageSize这是整数除法的向上取整技巧。如果totalCount41pageSize20(4119)/203正确。如果直接写totalCount/pageSize41/202最后一页就丢了。注意MSSQL 2012及以上版本支持OFFSET FETCH写法是ORDER BY BillID DESC OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY。但毕设环境里经常装的是SQL Server 2008 R2所以ROW_NUMBER方案更保险。4. 避坑与排查JSPMSSQL组合里最容易翻车的五个地方4.1 中文乱码从数据库排序规则到JSP pageEncoding的三层排查现象商品名称在数据库里查出来是“???”或者“锟斤拷”JSP页面显示乱码。原因MSSQL的排序规则Collation默认可能是SQL_Latin1_General_CP1_CI_AS不支持中文。JSP页面的pageEncoding和contentType没设对Tomcat的URIEncoding默认不是UTF-8。解决分三步查。第一步在MSSQL里执行SELECT SERVERPROPERTY(Collation)如果结果不含Chinese_PRC需要改数据库排序规则或者建库时指定COLLATE Chinese_PRC_CI_AS。第二步JSP文件头部加% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%三个属性缺一不可。第三步Tomcat的server.xml里Connector节点加URIEncodingUTF-8否则GET请求里的中文参数会乱码。如果是POST请求还需要在Servlet里加request.setCharacterEncoding(UTF-8)且这行必须在第一次getParameter之前调用。4.2 连接池耗尽为什么每次刷新页面都在新建数据库连接现象系统跑一会儿就报“无法获取连接”重启Tomcat后恢复过一会儿又不行。原因JSP页面里直接写DriverManager.getConnection()每次请求都新建连接用完没有close()。MSSQL默认最大连接数有限连接被占满后新请求就排队等死。解决用连接池。毕设环境里最简单的是在context.xml里配Tomcat JDBC PoolResource namejdbc/StockDB authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 maxWaitMillis10000 usernamesa passwordyourpassword driverClassNamecom.microsoft.sqlserver.jdbc.SQLServerDriver urljdbc:sqlserver://localhost:1433;databaseNameStockDB;encryptfalse/然后在JSP或Servlet里用DataSource ds (DataSource) new InitialContext().lookup(java:comp/env/jdbc/StockDB);获取连接。maxTotal20表示最多20个连接maxWaitMillis10000表示获取连接超时10秒。注意url里的encryptfalseMSSQL JDBC驱动新版本默认加密不加这个参数会报SSL错误。4.3 金额精度丢失DECIMAL转double的隐形陷阱现象采购单金额合计和手工算的对不上差几分钱。原因数据库里金额是DECIMAL(18,2)Java里用double接收double的二进制浮点表示无法精确存储0.1、0.2这样的十进制小数。累加时误差逐渐放大。解决Java里用BigDecimal不要用double。从ResultSet取值时用rs.getBigDecimal(Amount)运算时用bd1.add(bd2)而不是bd1 bd2。如果JSP页面需要显示用bd.setScale(2, RoundingMode.HALF_UP).toString()格式化。另外注意BigDecimal的equals()会比较精度1.0和1.00不相等比较金额是否相等要用compareTo()0。4.4 日期格式MSSQL的datetime和Java的java.util.Date互转现象入库日期在数据库里是2024-01-15 00:00:00.000JSP页面显示成“Mon Jan 15 00:00:00 CST 2024”。原因直接rs.getDate()拿到的是java.sql.DatetoString()格式不符合中文习惯。或者用SimpleDateFormat格式化时pattern写错。解决统一用SimpleDateFormat(yyyy-MM-dd HH:mm:ss)格式化。如果只需要日期用yyyy-MM-dd。注意MSSQL的datetime精度是3.33毫秒Java的java.util.Date精度是毫秒互转时会有微小误差对进销存业务来说可以忽略。如果从JSP表单提交日期字符串用new SimpleDateFormat(yyyy-MM-dd).parse(dateStr)转成Date再new java.sql.Timestamp(date.getTime())存入数据库。4.5 毕设源码里的硬编码数据库密码写在JSP里怎么改现象源码里的数据库连接写死在每个JSP页面顶部改密码要改几十个文件。原因毕设作者图省事每个页面都复制粘贴了同一段JDBC代码。解决抽出一个DBUtil类用静态方法获取连接。更规范的做法是用JNDI数据源见4.2。如果时间紧至少把所有连接信息放到一个db.properties文件里jdbc.urljdbc:sqlserver://localhost:1433;databaseNameStockDB;encryptfalse jdbc.usernamesa jdbc.passwordyourpassword然后用Properties.load()读取。注意properties文件放在WEB-INF/classes目录下不要放在Web根目录否则浏览器可以直接访问到密码就泄露了。5. 从毕设到能跑把这份源码改造成可演示系统的三个关键动作5.1 用MSSQL还原.bak文件并修正登录账号毕设.zip里通常有一个StockDB.bak文件。还原步骤打开SQL Server Management Studio右键“数据库”-“还原数据库”-“设备”-选择.bak文件-确定。还原后数据库里可能有一个默认的登录用户但密码未知。这时候用Windows身份验证登录执行ALTER LOGIN sa WITH PASSWORD NewPassword123; ALTER LOGIN sa ENABLE;如果sa被禁用ENABLE一下。然后在Java代码里把连接字符串的username和password改成sa和新密码。注意MSSQL默认只允许Windows身份验证需要右键服务器-属性-安全性-选择“SQL Server和Windows身份验证模式”重启MSSQL服务。5.2 在IDEA里新建JSP项目并导入源码的目录结构很多人拿到源码后不知道怎么在IDEA里跑起来。正确姿势File-New-Project-Java Enterprise-勾选Web Application。然后把源码里的src目录下的Java文件复制到新项目的src下WebContent或web目录下的JSP、CSS、JS复制到新项目的web下。注意WEB-INF/lib下的jar包要一起复制通常包括mssql-jdbc.jar、jstl.jar、standard.jar。然后在Project Structure-Modules-Dependencies里确认这些jar包已加入。最后配置Tomcat运行Application context设成/Stock浏览器访问http://localhost:8080/Stock/login.jsp。5.3 用SQL Server Profiler抓慢查询并加索引系统跑起来后如果库存列表加载超过3秒用SQL Server Profiler跟踪一下。新建跟踪-选择Events: RPC:Completed和SQL:BatchCompleted-运行-在系统里点一下库存查询-Profiler里会显示执行的SQL和Duration。如果发现StockBill表的查询很慢加索引CREATE NONCLUSTERED INDEX IX_StockBill_ProductID_BillType ON StockBill(ProductID, BillType) INCLUDE(Quantity, BillDate);这个索引覆盖了“查某商品某类型的单据”这个高频查询INCLUDE里的字段让查询不用回表。加完索引后再用Profiler看Duration通常能从几百毫秒降到几毫秒。注意索引不是越多越好每个索引都会拖慢插入速度StockBill表如果插入频繁索引控制在3个以内。我自己的习惯是拿到任何一份毕设源码先不改代码先把它跑起来用Profiler看一遍所有SQL把没加索引的外键列都加上索引然后再动业务逻辑。这样后面改代码时至少不会因为数据库慢而怀疑是自己改错了。希望帮到你。本文还有配套的精品资源点击获取
返回列表