资讯动态

JavaEE+Oracle仓库管理系统实战:表结构、事务与部署要点

发布时间:2026/9/14 3:24:07 来源:尧图企业网站定制
简介这是一套基于JavaEEOracle实现的仓库管理系统毕业设计资源定位为适合毕业设计、课程设计或工程实训的完整项目适合Java Web初学者及进阶学习者巩固分层架构开发能力。系统涵盖入库、出库、商品查看、用户注册、个人信息管理五大功能模块业务链路清晰可帮助理解企业级MVC架构下的数据流转与会话管理。资源包共191个文件、大小60.22MB核心代码为31个Java源文件与JSP页面配合SQL数据库脚本可直接初始化Oracle表结构另含论文、答辩PPT及操作演示视频便于快速上手和二次开发。压缩包内还有CSS/JS静态资源、XML配置文件与工程部署说明目录结构完整省去环境搭建和排错时间。目前已有173人学习下载适合作为毕业答辩、课程设计或仓库类业务系统开发的参考范本。1. JavaEEOracle仓库管理系统里那套「老组合」的实际价值搜索这个标题的人一半是 JavaEE 课程设计要交差另一半是公司里需要一个能真实记账的进销存原型。JavaEE Oracle 被说“过时”但仓储场景最重的两件事——并发扣库存和单据可追溯——恰恰是它处理得最稳的地方。Oracle 的行级锁和事务隔离级别让“两个人同时出库同一件货”这类问题在数据层就被挡住而不是靠应用代码反复加锁。这个标题对应的是一套完整交付物源码、数据库 SQL 脚本、论文、操作视频。对工程师来说最有价值的不是照着视频点一遍而是看清四件事表结构怎么拆、三层代码怎么分工、Oracle 分页和事务怎么写、部署时报 ORA- 错误先查哪里。下面按这个顺序把系统讲透新手能照着改老手能直接抄边界处理。2. 先设计数据再写代码Oracle 表结构与库存约束2.1 仓库业务落到四张核心表一套能跑完入库、出库、盘点的仓库管理系统最少需要四张业务表商品信息表wms_product、入库单表wms_inbound、出库单表wms_outbound、库存表wms_stock再加一张用户表做登录。常见做法是不过度抽象直接把业务字段平铺因为课程设计和轻量落地场景里过度设计比少字段更致命。表名用途关键字段wms_product商品主数据product_id, product_code, product_name, spec, unit, price, statuswms_inbound入库单据inbound_id, order_no, product_id, quantity, create_by, create_timewms_outbound出库单据outbound_id, order_no, product_id, quantity, create_by, create_timewms_stock实时库存product_id, warehouse_id, stock_qty, locked_qty, updated_time商品表的建表脚本如下CREATE TABLE wms_product ( product_id NUMBER(10) NOT NULL, product_code VARCHAR2(30) NOT NULL, product_name VARCHAR2(120) NOT NULL, spec VARCHAR2(60), unit VARCHAR2(10) DEFAULT 件, price NUMBER(12,2) DEFAULT 0, status CHAR(1) DEFAULT 1, CONSTRAINT pk_wms_product PRIMARY KEY (product_id), CONSTRAINT uk_wms_product_code UNIQUE (product_code) );字段说明product_code 必须建唯一约束仓库里扫码和手工录入都靠这个编号定位商品重复编号会让后续所有单据对不上账price 用 NUMBER(12,2) 而不是 FLOAT12 位精度里整数部分 10 位、小数 2 位金额运算不会出现浮点尾巴status 用 CHAR(1) 存 0/1应用层约定 1 为上架可出库。VARCHAR2 的长度按“编码前缀流水号批次”的最坏情况预估不要拍脑袋给 200。2.2 Oracle 没有自增主键序列加触发器Oracle 没有 MySQL 的 auto_increment最标准的替代方案是序列SEQUENCE负责生成数字触发器TRIGGER在 INSERT 前把 NEXTVAL 写进主键列。这套系统的 SQL 脚本里通常也是这么做的。CREATE SEQUENCE seq_product_id START WITH 1001 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER trg_product_id BEFORE INSERT ON wms_product FOR EACH ROW BEGIN IF :NEW.product_id IS NULL THEN :NEW.product_id : seq_product_id.NEXTVAL; END IF; END;参数说明START WITH 从 1001 开始给手工导入的历史数据留出 1000 以下的空间避免主键冲突INCREMENT BY 1 是默认步长不要改大否则日志里两个单号之间出现空洞对账时解释成本极高NOCACHE 适合开发期生产环境可改 CACHE 20 减少序列争用。触发器里判断 :NEW.product_id IS NULL 是刻意为之允许应用层主动塞主键这在数据迁移时非常有用。2.3 库存表加联合主键和行级锁库存永远是仓库系统的热点数据。入库线程加 10 件、出库线程减 5 件同时作用在同一商品上如果库存表设计成单字段无约束最终结果大概率是错的。这里用联合主键从结构上保证“一个商品在一个库位只有一行”。CREATE TABLE wms_stock ( product_id NUMBER(10) NOT NULL, warehouse_id NUMBER(6) NOT NULL, stock_qty NUMBER(10) NOT NULL DEFAULT 0, locked_qty NUMBER(10) DEFAULT 0, updated_time TIMESTAMP DEFAULT SYSTIMESTAMP, CONSTRAINT pk_wms_stock PRIMARY KEY (product_id, warehouse_id) );主键product_id, warehouse_id天然防住同一商品在同一库位出现多行。stock_qty 用 NUMBER(10) 而不是 INTEGER一方面 Oracle 的 INTEGER 底层就是 NUMBER(38)写 NUMBER(10) 反而显式限制了业务上限另一方面 NUMBER 在 SUM、AVG 聚合时不会做隐式转换。出库操作的 UPDATE 语句会带着主键条件执行Oracle 自动对它锁定的行加行级锁另一个事务的相同 UPDATE 会阻塞到前一个提交为止这比在应用层用 synchronized 可靠得多。3. 从登录到出库JavaEE 三层架构里的 Servlet 与 DAO3.1 分层规约JSP 不碰 SQLDAO 不写界面拿到这套源码第一件事是看包结构。标准的 JavaEE 分层是JSP 负责渲染Servlet 接收请求Service 写业务规则DAO 封装 JDBC实体类放在 vo 包里。JSP 里出现 JDBC 代码的项目后续加一个字段要改三处维护成本直接翻倍。src/main/java/com/wms/ ├── vo/ # User.java, Product.java, Stock.java ├── dao/ # UserDao.java, ProductDao.java, StockDao.java ├── service/ # UserService.java, StockService.java └── web/ # LoginServlet.java, InboundServlet.java, OutboundServlet.java各层的协作关系用一个 URL 到方法的映射表就能说清楚URLServletService 方法做的事/loginLoginServletuserService.login()校验账号密码写 Session/inboundInboundServletstockService.inbound()生成入库单并增加库存/outboundOutboundServletstockService.outbound()生成出库单并扣减库存/checkCheckServletstockService.check()盘点差异查询事务边界放在 Service 层而不是 DAO。一个出库动作要“减库存 写单据”两个 DAO 方法各自开会话的话减库存成功、写单失败库存就凭空消失了。把连接攥在 Service 手里统一提交回滚是这套小系统里唯一不能妥协的纪律。3.2 登录接口Session 闭合与 URL 保护登录流程不复杂但容易漏掉“未登录用户直接访问 index.jsp”这个口子。先看控制层WebServlet(/login) public class LoginServlet extends HttpServlet { private final UserService userService new UserService(); Override 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.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /index.jsp); } }逻辑说明login 失败用 forward 回到登录页登录成功用 sendRedirect 跳主页这两个动作的区别是前者保留 request 里的 msg 属性后者避免刷新页面时重复提交表单。密码比较必须在 Service 层做DAO 只负责按 username 查出用户不要把“拿参数拼 SQL”的逻辑写进 DAO。Session 写好后还要一个 Filter 挡住未登录访问WebFilter(/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.contains(/login) || req.getSession().getAttribute(loginUser) ! null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }注意 Filter 里的放行条件登录页和登录接口放行其余全部要求 Session 里有 loginUser。实际项目中还要加上静态资源后缀.js、.css、.png的放行否则登录页样式加载不出来这个坑在视频演示里最容易翻车。3.3 出库事务先锁行再减库存最后写单出库是这套系统里最值得抄的代码。正确顺序是开事务、锁定库存行、校验库存充足、扣减库存、写入出库单、提交任何一步失败都回滚。public void outbound(OutboundVO order) throws BusinessException { Connection conn getConnection(); PreparedStatement ps null; try { conn.setAutoCommit(false); String lockSql SELECT stock_qty, locked_qty FROM wms_stock WHERE product_id ? AND warehouse_id ? FOR UPDATE; // 执行查询并锁定行 String decSql UPDATE wms_stock SET stock_qty stock_qty - ?, updated_time SYSTIMESTAMP WHERE product_id ? AND warehouse_id ?; // 执行扣减这里需要先比较查询到的 stock_qty 是否足够 String insSql INSERT INTO wms_outbound(order_no, product_id, quantity, create_by, create_time) VALUES (?, ?, ?, ?, SYSDATE); // 执行插入单据 conn.commit(); } catch (SQLException e) { conn.rollback(); throw new BusinessException(出库失败: e.getMessage()); } finally { conn.setAutoCommit(true); closeQuietly(ps, conn); } }逻辑说明SELECT ... FOR UPDATE 是关键它把该商品在库存表里的行锁定第二个出库事务走到这里会等待直到第一个事务提交或回滚。扣减前必须拿第一步查出来的 stock_qty 和本次出库数量比较库存不足要直接抛业务异常不能依赖 UPDATE 的返回值判断。插入出库单放进同一个事务保证了“账实一致”。3.4 盘点差异一次 JOIN 查出对不上的货盘点不是改库存是先产生差异报表人工复核后再调账。查询用 LEFT JOIN 把“盘了但库存表里没有”的商品也带出来SELECT p.product_code, p.product_name, s.stock_qty AS system_qty, c.count_qty AS real_qty, c.count_qty - s.stock_qty AS diff_qty FROM wms_product p JOIN wms_stock s ON s.product_id p.product_id LEFT JOIN wms_check c ON c.product_id p.product_id WHERE ABS(c.count_qty - s.stock_qty) 0 ORDER BY diff_qty DESC;说明LEFT JOIN 保证已盘商品即使没在库存表建行也能出现在结果里WHERE 里用 ABS 大于 0 同时筛选盘盈和盘亏ORDER BY diff_qty DESC 让盘亏最多的排最前便于优先处理。生成报表后调账动作再走一次带事务的 UPDATE绝不允许在报表查询里顺手改数。4. Oracle 专属难点JDBC 连接、ROWNUM 分页与存储过程4.1 驱动加载与连接串参数这套系统的数据库脚本跑在 Oracle 上连接层最常出问题的是驱动类没加载、连接串写错两种。正确的写法是Class.forName(oracle.jdbc.driver.OracleDriver); String url jdbc:oracle:thin://localhost:1521/ORCL; String user wms; String password wms123; Connection conn DriverManager.getConnection(url, user, password);连接串有两种格式对应 Oracle 不同的寻址方式格式适用场景示例jdbc:oracle:thin:host:port:SIDOracle 11g 及更早SID 方式jdbc:oracle:thin:192.168.1.10:1521:ORCLjdbc:oracle:thin://host:port/service_nameOracle 12c 及以后服务名方式jdbc:oracle:thin://192.168.1.10:1521/orclpdb参数说明thin 是纯 JDBC 驱动不需要装 Oracle 客户端课程环境里最稳如果视频里用的是 PL/SQL Developer 连库那走的是本机 Oracle 客户端和 Java 程序无关。SID 与 service_name 混写是常见的 ORA-12505 报错根源连接前先用lsnrctl status确认服务名。4.2 ROWNUM 分页两层子查询的固定写法Oracle 11g 没有 MySQL 的 LIMIT分页查询靠 ROWNUM 实现。ROWNUM 是在结果集生成时分配的序号直接写WHERE ROWNUM 10查不出数据因为排序前 ROWNUM 还没稳定。标准写法是两层子查询SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT product_code, product_name, price FROM wms_product ORDER BY product_id ) t WHERE ROWNUM ? ) WHERE rn ?;参数说明内层子查询先排序中层子查询分配 ROWNUM 并截取“到第 pageSize 页为止”的最大行号外层再过滤掉前面的行。两个参数依次是pageNum * pageSize和(pageNum - 1) * pageSize例如第 2 页每页 10 条就是ROWNUM 20 AND rn 10。ORDER BY 必须放在最内层否则 ROWNUM 顺序会乱。这套 SQL 在 Oracle 11g、12c 上都成立是数据库 SQL 脚本里最值得粘贴复用的部分。4.3 库存预警存储过程参数与输出存储过程在仓库系统里适合做批量扫描类任务比如库存低于阈值的预警。把业务判断放进数据库Java 端只负责调用可以减少网络往返。CREATE OR REPLACE PROCEDURE sp_check_stock(p_min_qty IN NUMBER) AS BEGIN FOR r IN ( SELECT product_id, product_name, stock_qty FROM wms_stock s JOIN wms_product p ON p.product_id s.product_id WHERE s.stock_qty p_min_qty ) LOOP DBMS_OUTPUT.PUT_LINE(库存不足: || r.product_name || 剩余 || r.stock_qty); END LOOP; END;Java 端调用{call sp_check_stock(?) }参数用 CallableStatement 的 setInt 传入。注意 DBMS_OUTPUT 的输出在 SQL*Plus 里默认不显示要先执行SET SERVEROUTPUT ONJava 端如果想拿到行应该改成返回游标而不是 PUT_LINE。存储过程里避免直接 COMMIT把提交权留给调用方否则连同 JDBC 事务一起用时会出现诡异的提交点漂移。4.4 慢 SQL 验证EXPLAIN PLAN 看什么分页查询在数据量超过十万行后开始变慢先看执行计划再决定加什么索引EXPLAIN PLAN FOR SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT product_code, product_name FROM wms_product ORDER BY product_id ) t WHERE ROWNUM 100 ) WHERE rn 90; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY());执行计划里重点看两个东西TABLE ACCESS FULL 说明全表扫描数据量大时就要在 WHERE 和 ORDER BY 的列上建索引INDEX RANGE SCAN 是正常的索引范围扫描一般不用动。仓库系统的查询几乎都带 product_code 等值条件给外键列建普通 B-tree 索引就够不要滥用复合索引复合索引的列顺序写错反而让优化器放弃走索引。5. 部署与验收Oracle 监听、字符集与 ORA- 错误排查5.1 监听服务无法启动的处理顺序Oracle 监听服务OracleOraDb11g_home1TNSListener起不来是部署这套系统时最常遇到的第一个坎。按顺序做先看服务是否被禁用启动类型改成自动再打开 listener.ora 检查 HOST 是否写成了 localhost如果数据库只在本机用可以保留但局域网内其他机器用 JDBC 来连就必须改成实际主机名或 IP最后用lsnrctl start手动启动看报错。还有一个隐蔽原因主机名解析失败可以在 hosts 文件里加一行“本机IP 主机名”。改完 listener.ora 记得重启监听老版本 Oracle 不会自动重载配置。5.2 字符集不一致导致的中文乱码数据库字符集是 ZHS16GBKJDBC 连接默认按 UTF-8 传输中文进库就变成问号。最省事的做法是建库时就选 AL32UTF8如果现有库已经是 GBK代码里插入数据前用new String(value.getBytes(UTF-8), GBK)转码属于补丁式修复不推荐。推荐在连接 URL 上显式声明jdbc:oracle:thin://localhost:1521/ORCL?useUnicodetruecharacterEncodingUTF-8同时保证 JSP 页面pageEncodingUTF-8、数据库 AL32UTF8三处一致后中文问题基本绝迹。5.3 验收清单照着做一遍再写论文拿到这套源码后先用三件事验证它是不是“能跑且没偷工减料”。第一向 wms_product 批量插入一千行测试数据跑 4.2 的分页 SQL 翻到最后一页确认没有数据丢失或重复第二同时开两个浏览器对同一商品执行出库一个提交前另一个卡住提交后另一个自动放行说明行级锁生效第三重启 Tomcat 和 Oracle 服务确认登录后的 Session 失效但库存数据还在单据流水不丢。这三条过了再补论文里的测试章节就有真实数据支撑。部署时如果报 ORA-00942先查脚本执行顺序是不是表还没建就插数据报 ORA-01017 则检查用户名密码与连接串里的 service_name 是否匹配。把这几条记进验收清单比照着视频重敲一遍命令更能帮你理解这套系统。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价