资讯详情

资讯详情

加油站进销存管理系统设计与实现:表结构、事务与损溢盘点

简介一份加油站进销存管理系统设计与实现的毕业设计论文文档面向计算机相关专业学生、毕业设计课题研究者以及成品油零售行业信息化设计人员系统梳理了从可行性分析、系统分析到功能设计、技术落地的完整思路尤其聚焦采购、销售、库存三大核心环节如何借助信息系统提升业务效率和服务水平。文档为单个doc文件约343KB正文包括摘要、可行性研究、系统分析、组织结构与功能结构分析等章节并具体给出Visual Basic 6.0结合SQL Server 2008的技术选型以及三层架构、安全权限、Redis缓存、负载均衡等设计要点。已有75人学习阅读适合用于论文参考、开题准备或加油站进销存系统的初步设计借鉴也可帮助读者快速理解中小型管理信息系统的构建方法与实施重点掌握从需求分析到系统落地的规范流程。1. 进销存管理系统在加油站场景下先想清楚“销”和“存”的口径第一次接触“加油站进销存管理系统的设计与实现”这个题目时很容易把它当成普通的小超市进销存来抄作业。真正落下去才发现加油站有三个特征逼着你重新设计油品按升进、按升出但实际入库还要看密度换算成质量油罐不像货架看不到剩余数量只能靠液位仪或人工计量销售是跑在加油枪上的最高峰时十几条枪同时扣减同一种油品的库存。所以这个系统里的核心不是“商品”而是“油罐账”。这套系统适合两类人入场一类是在做 JSPServlet 课程设计的学生需要一份能答辩、能跑通的完整设计另一类是给民营油站做管理软件外包的工程师想找一个比 Excel 更稳固的进销存骨架。接下来我按最常见的 JSPServletMySQL 三层方案把表结构、事务处理、损溢盘点和部署验证一次讲透。2. 加油站进销存管理系统的表结构设计先分清“油品”和“油罐”进销存管理系统里最忌讳把“商品”和“库位”塞进一张表。加油站尤其明显同样 92 号汽油可能存在地埋罐 A、地埋罐 B 两个罐里进油时可以指定进到哪个罐但卖给客户时不会区分是哪条枪从哪个罐出的。所以表结构要能表达“油品是一种商品油罐是物理容器枪是销售设备”这三个层次。2.1 基础数据表油品、油罐、加油枪油品表保存油品名称和计量单位单位建议直接存升不要存成可变的“吨/升”双单位否则报表转换会很麻烦。油罐表挂在油品下记录罐体容量和当前液位对应的体积系数。加油枪表挂在油罐下记录枪号、连接的罐 ID 和状态。以下是核心 SQLCREATE TABLE oil_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 油品名称如92号汽油, unit VARCHAR(10) DEFAULT 升, price DECIMAL(10,2) DEFAULT 0 COMMENT 当前零售价, status TINYINT DEFAULT 1 ); CREATE TABLE oil_tank ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, tank_no VARCHAR(20) NOT NULL COMMENT 罐编号如T-01, capacity DECIMAL(12,2) DEFAULT 0 COMMENT 罐容升, current_volume DECIMAL(12,2) DEFAULT 0 COMMENT 当前油高换算体积升, FOREIGN KEY (product_id) REFERENCES oil_product(id) ); CREATE TABLE fuel_gun ( id INT PRIMARY KEY AUTO_INCREMENT, tank_id INT NOT NULL, gun_no VARCHAR(10) NOT NULL COMMENT 枪号, is_active TINYINT DEFAULT 1, FOREIGN KEY (tank_id) REFERENCES oil_tank(id) );注意油罐表的current_volume是一个快照值每次人工计量或液位仪上报时更新它不代表账面库存只代表实存。这套设计的核心思想是账面库存靠流水累计实存靠盘点更新两者差异就是损溢。2.2 进销存流水表把每次业务落成不可修改的凭证进油单、销售单、损溢单是三条并行的流水线。进油单从供应商来销售单从加油枪来损溢单从盘点来。我一般会把它们分开建表不合并成一张“库存变动表”因为业务字段差异太大合并后查询要不断判断类型。CREATE TABLE purchase_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL UNIQUE, supplier_name VARCHAR(100), product_id INT NOT NULL, tank_id INT NOT NULL, volume DECIMAL(12,2) NOT NULL COMMENT 进油体积升, density DECIMAL(6,4) DEFAULT 0.7300 COMMENT 进油密度, amount DECIMAL(12,2) COMMENT 进货金额, purchase_time DATETIME NOT NULL, status TINYINT DEFAULT 1, remark VARCHAR(200) ); CREATE TABLE sale_record ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL UNIQUE, gun_no VARCHAR(20) NOT NULL, product_id INT NOT NULL, tank_id INT NOT NULL, volume DECIMAL(12,2) NOT NULL COMMENT 加油升数, unit_price DECIMAL(10,2) NOT NULL, total_amount DECIMAL(12,2) NOT NULL, sale_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;进油单要记录密度因为加油站进货时油罐车会带来密度检测单同样的体积在不同温度下质量不同。销售单直接通过枪号反查油罐这样报表里可以按罐汇总出枪销量。order_no必须唯一而且要按时间生成比如PO20240605130000防止并发时主键冲突。2.3 库存余额表与盘点表有了流水还需要一张“产品库存余额表”来算账面库存否则每次查库存都要SUM(sale_record.volume)数据量一大就慢。余额表只存每个油品在某个罐下的累计应收库存CREATE TABLE stock_balance ( product_id INT NOT NULL, tank_id INT NOT NULL, book_volume DECIMAL(12,2) DEFAULT 0 COMMENT 账面库存升, update_time DATETIME, PRIMARY KEY (product_id, tank_id) );盘点表记录每次实存与账存的差异以及责任描述。加油站的损耗往往不是偷油而是油气挥发、管道泄漏、温度变化所以盘点单要单独留存CREATE TABLE stock_check ( id INT PRIMARY KEY AUTO_INCREMENT, tank_id INT NOT NULL, check_time DATETIME NOT NULL, book_volume DECIMAL(12,2) NOT NULL, actual_volume DECIMAL(12,2) NOT NULL, diff_volume DECIMAL(12,2) NOT NULL COMMENT 正数为盘盈负数为盘亏, loss_reason VARCHAR(200), operator VARCHAR(30) );这套表结构能顺带回答课程设计答辩时的高频问题“为什么油品表和油罐表分离”因为一个油品可以存在于多个罐一个罐只装一种油这是典型的一对多关系。如果直接把油品名称写进油罐表后续调整油品价格时会非常痛苦。3. 用 JSPServlet 实现加油站进销存的核心流程事务放在 Service 层表定好后开始写 Java 代码。老项目用 JDBC 直接连数据库新一点的会用连接池。这里我用 DBCP 连接池的写法JSP 只做展示Servlet 做控制DAO 做数据库操作。关键是进货入库和销售扣减库存这两个流程必须放到一个事务里否则库存余额会漂移。3.1 进货入库的 Servlet 实现进货入库要做的动作是写入purchase_order更新stock_balance.book_volume同时更新oil_tank.current_volume作为罐存参考。这三步任何一步失败都要回滚。下面是一个精简版的 Servlet 片段WebServlet(/purchase/add) public class PurchaseAddServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String productId req.getParameter(productId); String tankId req.getParameter(tankId); String volume req.getParameter(volume); String density req.getParameter(density); String supplier req.getParameter(supplier); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入进油单 String insertSql INSERT INTO purchase_order(order_no, supplier_name, product_id, tank_id, volume, density, purchase_time) VALUES(?,?,?,?,?,?,NOW()); PreparedStatement ps conn.prepareStatement(insertSql); ps.setString(1, generateOrderNo()); ps.setString(2, supplier); ps.setInt(3, Integer.parseInt(productId)); ps.setInt(4, Integer.parseInt(tankId)); ps.setBigDecimal(5, new BigDecimal(volume)); ps.setBigDecimal(6, new BigDecimal(density)); ps.executeUpdate(); // 2. 更新账面库存加行锁防止并发 String updateBalSql UPDATE stock_balance SET book_volume book_volume ? WHERE product_id ? AND tank_id ?; PreparedStatement ps2 conn.prepareStatement(updateBalSql); ps2.setBigDecimal(1, new BigDecimal(volume)); ps2.setInt(2, Integer.parseInt(productId)); ps2.setInt(3, Integer.parseInt(tankId)); ps2.executeUpdate(); // 3. 更新油罐快照 String updateTankSql UPDATE oil_tank SET current_volume current_volume ? WHERE id ?; PreparedStatement ps3 conn.prepareStatement(updateTankSql); ps3.setBigDecimal(1, new BigDecimal(volume)); ps3.setInt(2, Integer.parseInt(tankId)); ps3.executeUpdate(); conn.commit(); resp.sendRedirect(purchaseList.jsp); } catch (Exception e) { if (conn ! null) conn.rollback(); e.printStackTrace(); req.setAttribute(error, 入库失败 e.getMessage()); req.getRequestDispatcher(purchaseAdd.jsp).forward(req, resp); } finally { DBUtil.closeQuietly(conn); } } }这段代码的要点有三个conn.setAutoCommit(false)之后的任何executeUpdate都是临时的只有commit才生效库存余额更新用book_volume book_volume ?而不是先查后写避免了两个请求同时读到旧值再写回导致丢更新最后finally里要关闭连接否则 Tomcat 连接池很快耗尽。generateOrderNo()一般用时间戳加随机数生成格式如POyyyyMMddHHmmss三位随机数。3.2 销售扣减库存的 Servlet 事务处理销售流程是进销存管理系统的高频操作也是坑最多的地方。加油枪每完成一笔交易系统就要插入一条sale_record并让对应油品的账面库存减少。这里有个业务决策是先扣库存还是先记流水我一般会先插入流水再扣库存因为流水是客观事实扣库存是汇总结果。String insertSaleSql INSERT INTO sale_record(order_no, gun_no, product_id, tank_id, volume, unit_price, total_amount, sale_time) VALUES(?,?,?,?,?,?,?,NOW()); PreparedStatement ps conn.prepareStatement(insertSaleSql); ps.setString(1, generateSaleNo()); ps.setString(2, gunNo); ps.setInt(3, productId); ps.setInt(4, tankId); ps.setBigDecimal(5, volume); ps.setBigDecimal(6, unitPrice); ps.setBigDecimal(7, volume.multiply(unitPrice)); ps.executeUpdate(); String deductSql UPDATE stock_balance SET book_volume book_volume - ? WHERE product_id ? AND tank_id ? AND book_volume ?; PreparedStatement ps2 conn.prepareStatement(deductSql); ps2.setBigDecimal(1, volume); ps2.setInt(2, productId); ps2.setInt(3, tankId); ps2.setBigDecimal(4, volume); int rows ps2.executeUpdate(); if (rows 0) { throw new RuntimeException(账面库存不足无法完成销售); }扣减语句里加了AND book_volume ?这个条件如果账面库存不足rows会是 0这时直接抛异常让整个事务回滚销售记录也不会落库。这是防止超卖最简单的手段比先查询再判断要更安全。注意加油站在高峰期同时有多个线程扣同一个油品库存这条UPDATE会触发行锁后到的请求会等待性能上完全能接受。如果业务要求加油枪离线也能卖那就需要引入预授权额度那是另一个复杂度级别课程设计不需要做。3.3 DAO 层的隐藏要点进销存管理系统的 DAO 层不建议把 SQL 字符串散落在 Servlet 里。常见做法是建PurchaseDAO、SaleDAO、StockCheckDAO每个 DAO 只负责一张表的增删改查。Servlet 里调SaleDAO.insert(saleRecord)和StockDAO.deductVolume(productId, tankId, volume)。这两个方法要放在同一个事务里所以 DAO 方法不能自己开连接要由上层传入Connection。我见过很多课程设计把DBUtil.getConnection()写在每个 DAO 方法里结果 Servlet 层 split 了事务数据一乱就查不出问题。这里给一个简单的 DAO 方法签名示例public void insertSale(Connection conn, SaleRecord record) throws SQLException { String sql INSERT INTO sale_record...; try (PreparedStatement ps conn.prepareStatement(sql)) { // 设置参数 } }这样每次业务都是一个事务脚本结构上类似于一个庞大 Service 类里的多个方法。如果你的代码量超过 500 行就该考虑拆成多个 Service 类了。但对于课程设计或小油站项目这种度刚好。4. 加油站进销存的损溢盘点和报表用 SQL 把账算平进销存系统能不能在一个月后把账面库存和实际罐存对上全看盘点怎么设计。普通商超盘点是数货架加油站没法数油只能靠液位仪读数或者人工量油高然后换算成体积。这个环节如果不用系统约束Excel 也能做但系统要做的是把“账面数”和“实际数”的差异自动计算出来生成损溢单。4.1 盘点损益的计算流程盘点时先选择某个油罐系统实时读取出它的账面库存然后录入实际罐存。差异值diff_volume actual_volume - book_volume。如果是负数说明盘亏要进入损耗分析如果正数可能是上次进油时的管道余油或温度变化导致体积膨胀。生成损溢单之后还要把stock_balance的账面数修正为实际数否则下个月会差得更多。-- 盘点审核时执行把账面库存调整为实际库存 UPDATE stock_balance sb JOIN stock_check sc ON sb.product_id (SELECT product_id FROM oil_tank WHERE id sc.tank_id) AND sb.tank_id sc.tank_id SET sb.book_volume sc.actual_volume WHERE sc.id ?;这段 SQL 依赖stock_check表中的tank_id和actual_volume注意要先插入盘点单再执行这个更新两条操作必须在同一个事务中。部分教程会把actual_volume直接写入oil_tank.current_volume那样会丢失盘点历史的可追溯性所以我不推荐。4.2 进销存日报表一张 SQL 查完进货、销售、结存日常运营最关心的是日报表每种油品今天进了多少、卖了多少、账面还有多少。传统进销存报表用 UNION 把进货和销售拼起来这里给出一个可以直接写在 JSP 里的统计 SQLSELECT p.name AS product_name, IFNULL(po.in_volume, 0) AS total_in, IFNULL(sr.out_volume, 0) AS total_out, (sb.book_volume IFNULL(sr.out_volume, 0) - IFNULL(po.in_volume, 0)) AS opening_volume, sb.book_volume AS closing_volume FROM oil_product p LEFT JOIN ( SELECT product_id, SUM(volume) AS in_volume FROM purchase_order WHERE DATE(purchase_time) CURDATE() GROUP BY product_id ) po ON p.id po.product_id LEFT JOIN ( SELECT product_id, SUM(volume) AS out_volume FROM sale_record WHERE DATE(sale_time) CURDATE() GROUP BY product_id ) sr ON p.id sr.product_id LEFT JOIN stock_balance sb ON p.id sb.product_id;这里有个计算技巧期初库存 期末账面库存 今日销售 - 今日进货。因为stock_balance是实时余额查询时它已经包含了今天的进出所以反向推回昨夜的库。这个报表能看出哪个油品今天卖了 80% 以上需要补货。如果你的系统里还有调拨单、损耗单报表就要把这些也加进UNION ALL但课程设计做到这一步一般就够加分了。4.3 油品价格调整的版本记录加油站汽油价格几乎每周都在调如果直接在oil_product.price字段上改历史销售记录的销售额就没法复盘。常见改进方案是建立price_history表记录生效时间。销售时通过时间取当时价格而不是读油品表当前价格。这种做法对进销存管理系统来说叫“业务快照”代码实现也不难CREATE TABLE price_history ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, old_price DECIMAL(10,2), new_price DECIMAL(10,2), change_time DATETIME );销售流水里的unit_price应当写入当时从price_history查出来的价格这样日后的利润报表才是真实历史成本。如果直接在销售代码里SELECT price FROM oil_product油价一调昨天的报表就变样了。这个细节我在给油站调报表时经常碰到属于“不会报错但逻辑错误”的经典问题。5. 进销存管理系统的部署验证与三个易错点自查系统代码写完真正要上线或交作业前花半小时做一轮验证比多写两百行代码更有价值。很多进销存项目失败不在功能缺而在数据对不上。下面给出我常用的部署检查和参数验证步骤。先看部署把项目打成 WAR 包放到 Tomcatwebapps下启动后访问http://localhost:8080/fuelStation/。如果出现 404先检查web.xml里的welcome-file是否指向login.jsp再检查 Servlet 的注解路径是否带上了项目上下文路径。数据库连接配置在DBUtil.java里常见写法是private static final String URL jdbc:mysql://localhost:3306/fuel_station?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456;三个易错点值得花时间自查。第一中文乱码JSP 页面顶部要加% page contentTypetext/html; charsetUTF-8 %Servlet 接收参数前要req.setCharacterEncoding(UTF-8)此外 MySQL 连接串里characterEncodingutf8不能省略三个地方少一个油品名称就会变成问号。第二库存对不上先执行一个简单验证——把某油罐今天的销售流水SUM(volume)加上当前stock_balance.book_volume看是否等于昨结余加今日进货。如果不等于去查有没有未提交事务或重复插入。事务没提交的典型症状是应用日志有记录但数据库表里没数据或数据库有数据但用 Navicat 看不到锁。第三连接池耗尽如果并发测试时报Cannot get a connection, pool exhausted检查 DAO 里是不是没有关闭ResultSet和Statement连接只关了Connection是不够的务必使用 try-with-resources 或finally关闭所有资源。最后再让系统跑一次完整流程进货 1000 升销售 300 升盘点输入 698 升系统自动算出盘亏 2 升。这样一套“进→销→存→盘”的闭环验证下来进销存管理系统才算真正立住了。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →