ARTICLE DETAIL

资讯详情

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

SSM+JSP构建仓库管理系统:从数据库设计到部署运维全解析

SSM+JSP构建仓库管理系统:从数据库设计到部署运维全解析 简介一套基于SSM框架与JSP技术的仓库管理系统源码包适用于Java Web学习者、毕业设计开发者及需要快速搭建库存管理项目的技术人员。项目完整整合Spring、SpringMVC、Mybatis三大框架并覆盖Maven依赖管理、Tomcat部署、MySQL数据库存储等核心环节能够直观呈现数据访问层、业务逻辑层与页面交互层的标准分层实现适合作为SSM整合学习的范例。资源共564个文件压缩包60.79MB包含Java源码、JSP页面、XML配置、JAR依赖库、SQL数据库脚本、Excel数据表格以及MP4演示视频等前后端文件齐全目录结构清晰便于对照学习。已有1731人学习下载。借助演示视频和完整源码可快速掌握仓库入库、出库、库存查询等核心功能的设计思路并在此基础上进行功能扩展或二次开发是SSM整合实践与Java Web项目实战中颇具参考价值的完整资料。1. 仓库管理系统选型为什么 SSM JSP 依然是实用组合仓库管理系统在中小型企业的信息化清单里属于“看起来简单、做起来琐碎”的一类项目。出入库要记录库存要实时准确订单和采购要能追溯如果还要管多仓、批次、效期数据模型会迅速膨胀。很多团队在面对这类系统时容易陷入两种极端要么直接用 Excel 加共享文件夹凑合用但数据混乱要么一上来就上微服务、前后端分离开发周期拉长维护成本反而失控。用 SSMSpring SpringMVC MyBatis加 JSP 做仓库管理系统恰好落在两者之间。Spring 管对象生命周期和事务SpringMVC 管请求分发MyBatis 把 SQL 和 Java 方法映射起来JSP 在服务端渲染页面。这套组合不新但胜在结构清晰、上手门槛低、部署简单——打个 WAR 包丢进 Tomcat 就能跑。对于仓库这类以表单、列表、统计报表为核心的业务系统JSP 的服务端渲染反而减少了前后端联调成本一个 Tomcat 实例就能承载全部功能。这篇文章会从数据库建模、SSM 三层代码、JSP 页面交互、到部署和参数调优一条线展开。每段代码都可以直接复用参数会解释为什么这么设以及改错之后会出现什么症状。适合正在做课程设计、毕业设计或者要给小团队快速搭一套可用系统的开发者。2. SSM 仓库数据库设计表和字段决定业务边界2.1 仓库系统的核心实体与关系仓库管理系统的数据模型围绕“库存”这个核心展开。库存不是一张简单的表而是由商品、仓库、批次、出入库单据共同作用的结果。常见的实体包括用户、角色、仓库、货位、商品分类、商品、供应商、客户、入库单、入库明细、出库单、出库明细、库存表、库存流水。实体关系上商品和仓库是多对多关系通过库存表关联入库单和商品是多对多关系通过入库明细表关联库存流水记录每一次库存变动的来源单号、变动类型和变动数量。如果只设计商品表和出入库表不做库存快照和流水系统在并发场景下会出现超卖、负库存这类问题。下面是仓库管理系统最常用的核心表结构去掉了外键约束外键在应用层维护方便初始化时批量执行CREATE TABLE product ( id BIGINT NOT NULL AUTO_INCREMENT, sku_code VARCHAR(32) NOT NULL COMMENT 商品编码, name VARCHAR(128) NOT NULL COMMENT 商品名称, category_id BIGINT DEFAULT NULL COMMENT 分类ID, unit VARCHAR(16) DEFAULT 件 COMMENT 计量单位, price DECIMAL(10,2) DEFAULT 0.00 COMMENT 参考进价, min_stock INT DEFAULT 0 COMMENT 库存预警下限, status TINYINT DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sku (sku_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE warehouse ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, location VARCHAR(128) DEFAULT NULL, manager VARCHAR(32) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock ( id BIGINT NOT NULL AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存, locked_quantity INT NOT NULL DEFAULT 0 COMMENT 锁定库存, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_product_warehouse (product_id, warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_record ( id BIGINT NOT NULL AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, change_type TINYINT NOT NULL COMMENT 1入库 2出库 3盘点调整, change_quantity INT NOT NULL, before_quantity INT NOT NULL, after_quantity INT NOT NULL, order_no VARCHAR(32) DEFAULT NULL COMMENT 关联单据号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_product (product_id), KEY idx_order (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里要重点说明locked_quantity字段的设计意图。当销售订单提交但尚未真正出库时先把库存锁定避免其他订单把这一批货抢走。实际出库时再把锁定库存扣减如果订单取消则释放锁定。这种设计在进销存系统里称为“预占库存”没有这个字段高并发下单时会出现超卖。入库单和出库单的表结构是对称的CREATE TABLE inbound_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 入库单号, warehouse_id BIGINT NOT NULL, supplier_id BIGINT DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0草稿 1已入库 2已取消, remark VARCHAR(255) DEFAULT NULL, create_by BIGINT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE inbound_item ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) DEFAULT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;出入库单拆主表和明细表是为了支持一张单子对应多个商品这是进销存的基本形态。订单号用业务规则生成比如RK yyyyMMdd 四位序号不要用数据库自增 ID 直接当单号给业务人员看数字自增会暴露系统当日单量而且不便于按日期检索。2.2 初始化 SQL 脚本的写法和导入顺序建完表之后需要准备初始化数据。仓库系统最少需要一组账号、角色、一个默认仓库和少量演示商品。导入顺序上要先建库建表再插基础数据。很多初学者直接把整个 SQL 文件扔进 Navicat 执行如果表之间有外键就会报错所以上面特意去掉了外键。基础演示数据最少包含INSERT INTO user (id, username, password, real_name, role_id, status) VALUES (1, admin, MD5(admin123), 系统管理员, 1, 1), (2, keeper, MD5(keeper123), 仓管员, 2, 1); INSERT INTO warehouse (id, name, location, manager) VALUES (1, 主仓库, 一号库房A区, 张伟); INSERT INTO product (id, sku_code, name, category_id, unit, price, min_stock) VALUES (1, SKU001, A4复印纸, 1, 箱, 120.00, 10), (2, SKU002, 中性笔, 1, 盒, 15.50, 50);初始化数据要贴近实际业务这个细节会被演示视频放大。如果演示视频里录入商品时下拉框是空的、仓库选项为空整个系统的完成度在观感上会大打折扣。密码字段用 MD5 做演示没问题真实项目里至少要用 BCrypt 加盐JDK 自带的 MessageDigest 不适合直接用于生产。2.3 数据库连接配置里容易踩的坑SSM 项目里数据库配置写在jdbc.properties或applicationContext.xml中。以下是最常见的配置片段jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/warehouse?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456serverTimezoneAsia/Shanghai不加会报时区错误allowPublicKeyRetrievaltrue不加会在 MySQL 8 上连接失败。这两个参数是 MySQL 8 时代的标配。如果用 MySQL 5.x驱动类要改成com.mysql.jdbc.Driver但新项目完全没有理由继续用 5.x。3. 用 SSM 实现库存核心接口从 DAO 到 Service 的代码落地3.1 SpringMVC 分层结构与包命名SSM 项目的包结构一般按 Controller、Service、Mapper 三层划分。仓库管理系统的代码量不大但不建议把全部包放开推荐结构如下com.warehouse ├── controller # SpringMVC 控制器 ├── service # 业务接口和实现 ├── mapper # MyBatis 接口DAO层 ├── entity # 实体类 ├── dto # 请求/响应对象 ├── common # 通用返回结果、分页对象、常量 └── config # Spring 配置类Controller 层只做参数接收和结果封装不写业务逻辑Service 层处理事务和业务规则Mapper 层只负责 SQL。这条线在仓库系统里尤其重要因为入库、出库涉及多张表更新如果事务控制只放在 Controller 的 try-catch 里JSP 页面报错后会出现库存扣了但单据没生成的问题。3.2 入库业务Service 层的事务与库存扣减逻辑入库是仓库系统最核心的操作。逻辑分三步创建入库单主表记录、逐条写入入库明细、逐条更新库存表并写库存流水。三步必须在一个事务里任何一步失败都要回滚。对应的 Service 实现核心代码Service public class InboundServiceImpl implements InboundService { Autowired private InboundOrderMapper inboundOrderMapper; Autowired private InboundItemMapper inboundItemMapper; Autowired private StockMapper stockMapper; Autowired private StockRecordMapper stockRecordMapper; Override Transactional(rollbackFor Exception.class) public void createInboundOrder(InboundOrder order, ListInboundItem items) { // 1. 生成单号RK 时间戳 随机数 String orderNo RK System.currentTimeMillis(); order.setOrderNo(orderNo); order.setStatus(0); // 草稿状态 inboundOrderMapper.insert(order); // 2. 写入明细 for (InboundItem item : items) { item.setOrderId(order.getId()); inboundItemMapper.insert(item); // 3. 更新库存有则增加无则插入 Stock stock stockMapper.selectByProductAndWarehouse( item.getProductId(), order.getWarehouseId()); int beforeQty 0; if (stock null) { stock new Stock(); stock.setProductId(item.getProductId()); stock.setWarehouseId(order.getWarehouseId()); stock.setQuantity(item.getQuantity()); stock.setLockedQuantity(0); stockMapper.insert(stock); } else { beforeQty stock.getQuantity(); stock.setQuantity(stock.getQuantity() item.getQuantity()); stockMapper.updateQuantity(stock); } // 4. 写流水 StockRecord record new StockRecord(); record.setProductId(item.getProductId()); record.setWarehouseId(order.getWarehouseId()); record.setChangeType(1); record.setChangeQuantity(item.getQuantity()); record.setBeforeQuantity(beforeQty); record.setAfterQuantity(beforeQty item.getQuantity()); record.setOrderNo(orderNo); stockRecordMapper.insert(record); } } }Transactional(rollbackFor Exception.class)是必须的。Spring 默认只在遇到RuntimeException时回滚如果业务代码抛的是受检异常Exception不加rollbackFor的话事务不会回滚数据就会停留在“明细写了但库存没扣”的中间状态。这是面试里常问、实战里常犯的区别。UPDATE 库存的实际 SQL 也值得注意使用乐观锁防止并发覆盖update idupdateQuantity parameterTypecom.warehouse.entity.Stock UPDATE stock SET quantity #{quantity}, update_time NOW() WHERE id #{id} AND quantity #{oldQuantity} /updateWHERE条件里带上旧的quantity值如果更新影响行数为 0说明库存在读取后已经被其他事务修改这时由 Service 层重新查询并重试。这是无悲观锁情况下最廉价的并发控制手段。3.3 MyBatis 分页查询与模糊搜索配置仓库管理系统的列表页必然要支持分页和按商品名、编码搜索。这里使用 PageHelper 插件在 Spring 配置里加一行即可bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameplugins array bean classcom.github.pagehelper.PageInterceptor property nameproperties value helperDialectmysql reasonabletrue supportMethodsArgumentstrue /value /property /bean /array /property /beanreasonabletrue表示页码越界时自动修正到第一页或最后一页而不是直接报错。多了一个参数用户体验差别很大——仓库操作员点下一页点到没数据时页面空白会让他们认为系统出 bug 了。Controller 里接收分页参数的标准写法Controller RequestMapping(/product) public class ProductController { Autowired private ProductService productService; RequestMapping(/list) public String list(String keyword, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); ListProduct list productService.searchByKeyword(keyword); PageInfoProduct pageInfo new PageInfo(list); model.addAttribute(pageInfo, pageInfo); model.addAttribute(keyword, keyword); return product/list; } }PageHelper.startPage之后的第一个 MyBatis 查询会自动被拦截并拼上LIMIT语句。这里有个隐藏陷阱startPage必须紧跟查询语句中间不能插入任何其他 SQL 操作否则分页会作用到错误的查询上。4. JSP 页面交互实现列表渲染、表单提交与 JS 联动4.1 JSP 标准标签库渲染列表页仓库管理系统的页面数量通常在 20 个上下列表页是绝对主力。用 JSTL EL 表达式做服务端渲染是最常规的做法。核心代码模式如下% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html head title商品列表/title link relstylesheet href${pageContext.request.contextPath}/static/css/common.css /head body div classcontainer div classtoolbar form action${pageContext.request.contextPath}/product/list methodget input typetext namekeyword value${keyword} placeholder商品名称/编码 button typesubmit查询/button a href${pageContext.request.contextPath}/product/toAdd classbtn-primary新增商品/a /form /div table classgrid-table thead tr thID/th th商品编码/th th商品名称/th th单位/th th参考进价/th th库存/th th操作/th /tr /thead tbody c:forEach items${pageInfo.list} varitem tr td${item.id}/td td${item.skuCode}/td td${item.name}/td td${item.unit}/td tdfmt:formatNumber value${item.price} typecurrency//td td c:choose c:when test${item.stock ! null}${item.stock}/c:when c:otherwise span classtext-warning暂无库存/span /c:otherwise /c:choose /td td a href${pageContext.request.contextPath}/product/toEdit?id${item.id}编辑/a a hrefjavascript:void(0) onclickdeleteItem(${item.id})删除/a /td /tr /c:forEach /tbody /table !-- 分页 -- div classpagination c:if test${pageInfo.hasPreviousPage} a href${pageContext.request.contextPath}/product/list?pageNum${pageInfo.prePage}keyword${keyword}上一页/a /c:if span第 ${pageInfo.pageNum} / ${pageInfo.pages} 页/span c:if test${pageInfo.hasNextPage} a href${pageContext.request.contextPath}/product/list?pageNum${pageInfo.nextPage}keyword${keyword}下一页/a /c:if /div /div /body /html这段 JSP 有几个细节。${pageContext.request.contextPath}是为了防止项目部署在 Tomcat 的非根路径时静态资源和链接全部失效。fmt:formatNumber用来格式化金额避免显示 120.0 而不是 120.00。库存列的c:choose判断处理了 product 表没有冗余库存字段时列表联查得到 null 的情况。4.2 表单校验与 AJAX 提交的两种路线新增、编辑入库单这类表单页提交方式有两种传统表单同步提交和 AJAX 异步提交。传统方式代码简单Controller 返回redirect:/inbound/list缺点是校验错误要刷新整个页面。AJAX 方式用户体验更好但 JSP 页面里写 jQuery 的代码量和调试成本都上来了。推荐的做法是混合使用查询条件、分页导航用传统 GET 提交保证可收藏、可刷新新增/编辑数据用 AJAX 提交 JSON在 Controller 返回统一的结果对象。仓库入库单常见的 JS 联动逻辑是选择商品后自动带出单价和上一次入库价格便于和本次入库价对比。这个逻辑在 JSP 里通常这样写$(function () { // 监听商品选择变化 $(#productId).change(function () { var productId $(this).val(); if (!productId) return; // 发请求查询商品信息 $.get(contextPath /product/getById, {id: productId}, function (res) { if (res.code 200) { // 自动填充单价和上次入库价 $(#price).val(res.data.price); $(#lastInboundPrice).val(res.data.lastInboundPrice); // 自动计算金额 calcAmount(); } else { layer.msg(res.message || 查询失败); } }, json); }); // 数量和单价变化时重新计算金额 $(#quantity, #price).on(input, function () { calcAmount(); }); function calcAmount() { var qty parseFloat($(#quantity).val()) || 0; var price parseFloat($(#price).val()) || 0; $(#amount).val((qty * price).toFixed(2)); } });这段代码里商品下拉框选择、金额自动算、单价自动带出是一套完整的联动逻辑。不需要 Vue 或 React这种粒度在传统 JSP 项目里完全够用而且不会引入构建工具的复杂度。如果不小心把#productId的 change 事件绑定放到了 DOM 加载之前下拉框永远不会有联动效果。错误定位的方法是打开浏览器的 Network 面板看请求是否发出请求没发出说明事件绑定问题请求发出但没响应说明 Controller 或 Service 的问题。4.3 JSP 页面改动不生效的排查方法开发者经常遇到“JSP 代码改了但页面没变”的情况。这通常不是编译问题而是 Tomcat 对 JSP 预编译的缓存机制。排查步骤是清理 Tomcat 的 work 目录重启 Tomcat再用浏览器强制刷新CtrlF5。如果确认是 IDEA 部署的问题检查 Artifacts 配置里是否把 JSP 文件加入了编译输出。IDEA 中需要右键项目 - Open Module Settings - Artifacts在 Output Layout 中把 Web 资源目录关联到 exploded 包的对应位置。JSP 文件不像 Java 类那样走 Build 流程它是运行时被 Tomcat 解析的所以修改后必须重新部署或热部署。5. 部署到 Tomcat 与数据库脚本管理从源码到可运行5.1 使用 Maven 构建 WAR 包与依赖冲突处理SSM 项目通常用 Maven 管理依赖。pom.xml中需要关注的是版本对齐问题Spring、SpringMVC、MyBatis、MyBatis-Spring 这四个库的版本相互约束。推荐的一套稳定版本组合properties spring.version5.3.20/spring.version mybatis.version3.5.10/mybatis.version mybatis-spring.version2.0.7/mybatis-spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies构建命令是mvn clean package产出target/warehouse.war。如果构建失败高频原因是依赖下载不全或版本冲突。把本地 Maven 仓库的.lastUpdated后缀文件删掉重新拉或者换阿里云镜像源这两个操作能解决八成构建问题。5.2 部署到 Linux 服务器文件上传与重定向服务器部署流程可以用一句话概括上传 WAR 包到 Tomcat 的 webapps 目录重启服务。但实际环境的坑在于端口占用、目录权限和防火墙。以下是较完整的部署步骤# 1. 将 WAR 包上传到服务器以 CentOS 7 为例 scp target/warehouse.war rootyour-server:/opt/tomcat/webapps/ # 2. 切换到 Tomcat 用户并启动 cd /opt/tomcat/bin ./startup.sh # 3. 查看启动日志确认无报错 tail -f /opt/tomcat/logs/catalina.out # 4. 让防火墙放行 8080 端口 firewall-cmd --permanent --add-port8080/tcp firewall-cmd --reload启动后访问http://服务器IP:8080/warehouse/。出现 404 或白屏时先看 catalina.out 的最后 50 行日志数据库连接报错、端口被占用、Spring Context 初始化失败都会在这里体现。SQL 脚本管理上建议在项目根目录放sql/init.sql和sql/update.sql两个文件。init.sql用于全新环境初始化包含建库、建表、插入基础数据update.sql按时间顺序追加结构变更语句每条变更前加注释标记日期这样后续升级只执行 update 文件不需要重新初始化。5.3 Tomcat 内存参数对仓库系统的影响仓库系统本身并发量不大但报表查询和大列表导出时 JVM 内存容易吃紧。Tomcat 默认堆内存偏小遇到大批量导出 Excel 直接 OutOfMemoryError。推荐修改bin/catalina.sh中的JAVA_OPTSJAVA_OPTS-Xms512m -Xmx1024m -XX:MaxMetaspaceSize256m -Dfile.encodingUTF-8-Xms和-Xmx设置堆内存初始值和最大值这两项不设等时JVM 会频繁扩容回收响应时间波动明显。-Dfile.encodingUTF-8防止 Linux 默认编码导致中文乱码。这组参数对 2G 内存的虚拟机完全够用不需要盲目调大。6. 报表导出与库存预警两个立刻能用的进阶改进6.1 用 POI 把库存列表导出 Excel避免前端插件需求方经常要“把数据导出来看看”。JSP 页面配合 Apache POI在 Controller 里直接写输出流即可。这是 JSP 项目的标准做法代码路径短且稳定RequestMapping(/export) public void exportStock(HttpServletRequest request, HttpServletResponse response) throws IOException { ListStockVO list stockService.selectAllWithProduct(); // 设置响应头让浏览器弹出下载框 response.setContentType(application/vnd.ms-excel); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(库存报表.xlsx, UTF-8); response.setHeader(Content-disposition, attachment;filename fileName); // 创建工作簿并写入数据 XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(库存); String[] headers {商品编码, 商品名称, 仓库, 当前库存, 库存预警下限, 状态}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowIdx 1; for (StockVO vo : list) { Row row sheet.createRow(rowIdx); row.createCell(0).setCellValue(vo.getSkuCode()); row.createCell(1).setCellValue(vo.getProductName()); row.createCell(2).setCellValue(vo.getWarehouseName()); row.createCell(3).setCellValue(vo.getQuantity()); row.createCell(4).setCellValue(vo.getMinStock()); row.createCell(5).setCellValue(vo.getQuantity() vo.getMinStock() ? 库存不足 : 正常); } // 设置列宽避免打开 Excel 时列宽被挤压 sheet.setColumnWidth(0, 15 * 256); sheet.setColumnWidth(1, 20 * 256); sheet.setColumnWidth(2, 12 * 256); workbook.write(response.getOutputStream()); workbook.close(); }关键点是URLEncoder.encode处理中文文件名不处理的话 IE 和部分浏览器下载的文件名会乱码。XSSFWorkbook 是 xlsx 格式数据量五千行以内没问题超过这个量再考虑 SXSSF 流式写入。JSP 页面对应的只是一个导出按钮a href${pageContext.request.contextPath}/stock/export classbtn-export导出库存报表/a不需要任何前端插件后端直接把文件流推给浏览器这个方案最稳妥。实际开发中经常被忽视的一个坑是导出接口不能被权限拦截器拦截否则点导出后浏览器收不到任何反馈用户会认为系统没反应。6.2 定时任务扫描预警商品用表驱动替代人工巡检库存预警不需要实时推送每分钟扫描一次就够。用 Spring 自带的Scheduled注解即可不需要引入 QuartzComponent public class StockWarningTask { Autowired private StockService stockService; Autowired private WarningRecordMapper warningRecordMapper; // 每 60 秒执行一次fixedDelay 表示上次执行完后再等 60 秒 Scheduled(fixedDelay 60000) public void scanWarningStock() { ListStockVO lowStockList stockService.selectLowStockList(); for (StockVO vo : lowStockList) { // 检查今天是否已提醒过避免短信轰炸 boolean warned warningRecordMapper.checkTodayWarned(vo.getProductId()); if (!warned) { warningRecordMapper.insert(vo); // 实际项目中这里可以发站内信、企业微信通知 } } } }在 Spring 配置类或 XML 中启用task:annotation-driven/fixedDelay和cron的区别需要说明。fixedDelay是上次任务结束后固定延迟执行适合不希望任务堆积的场景cron是按绝对时间触发适合每天凌晨做日结这种固定时刻的任务。预警任务如果上一条还没执行完下一轮会排队所以用fixedDelay更合适。记录今天是否已提醒的检查避免库存持续低于下限时每分钟发一次预警把通知渠道打爆。数据库脚本层面为预警增加一张记录表即可查询今天是否已提醒的 SQL 用DATE(create_time) CURDATE()做条件。这种把提醒状态落表的设计比在内存里维护一个 Set 要可靠得多服务重启后不会重复提醒。至此仓库管理系统从数据库建模、SSM 三层实现、JSP 页面交互到部署上线和进阶改进形成了一条完整的落地链路。最后值得再做的一件事是写一份 README把初始化数据库的命令、默认账号密码、Tomcat 版本要求写清楚这会让源码交付的可用性提升一个档次。本文还有配套的精品资源点击获取
返回列表