news 2026/9/28 1:18:37

基于Java的加油站信息管理系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Java的加油站信息管理系统设计与实现

简介:基于Java的加油站信息管理系统毕业设计资源包,面向计算机相关专业学生与Java Web开发者,针对加油站日常运营中的油品、客户、交易等业务场景,提供完整系统实现。项目采用模型-视图-控制器架构模式,将业务逻辑、界面展示与控制调度分离,并运用Java服务页面技术生成动态网页,配合JDBC数据库访问接口完成数据存取,基于浏览器/服务器结构设计;同时体现面向对象设计思想,将油品、客户、交易等现实实体抽象为类,通过实例化与消息传递完成业务流转。压缩包约一点一九兆字节,主要包含论文文档、数据库脚本、安装配置说明及完整源代码目录,论文涵盖选题背景、技术选型、功能模块与实现过程,数据库脚本用于创建表结构和初始数据,源码按模型-视图-控制器组织,各层职责清晰。已有五百三十人学习浏览,适合用于毕业设计选题参考、Java Web课程实践或加油站信息管理系统的二次开发,有助于理解分层架构、前后端交互及数据库操作链路。

1. 这个系统到底在做什么:从加油员的日常工作倒推需求

加油站信息管理系统不是让你去写多炫的算法,它要解决的是一线加油员每天都会遇到的账和货的问题:油品进了多少、卖出多少、库存还剩多少,哪个员工在什么时间开了哪笔单,会员卡余额还剩多少钱。这个基于 java 的加油站信息管理系统,本质上是一个面向桌面场景的信息管理系统,把加油站的进销存、交接班和会员消费记录从手写台账搬进 Java 程序里,让老板不用等月底才对得上账。

对课设来说,它也是最典型的 Java 课程设计案例源码形态:有面向对象建模、有 JDBC 增删改查、有事务、有报表。适合已经学完 Java 基础、想找一个业务闭环完整又能写进简历的题目。反直觉的是,答辩时被问倒的往往不是功能多不多,而是 Java 基础细节——金额为什么不用 double、并发开单怎么保证数据一致性、连接池和事务的关系是什么。这些才是这个系统真正的主线。

2. 技术选型与工程结构:为什么毕业设计最常见的是 Java Swing + MySQL

2.1 三条技术路线怎么选

信息管理系统常做的实现方案有三条:Java Swing/JavaFX + MySQL 的桌面程序,Spring Boot + Thymeleaf 的服务端渲染页面,以及 Spring Boot + Vue 的前后端分离。题目只写了“基于 Java”时,我一般建议优先做桌面程序,而不是网页版。

原因很实际。第一,题目没有出现 Web、Spring Boot 等限定词时,桌面程序完全符合题意,演示时不用开两个终端,不用配跨域,老师现场看的时候双击一个启动类就能跑。第二,JFrame 写起来琐碎,但每个窗口都对应一个真实业务场景,指导老师翻代码时容易跟上思路;Spring Boot 版本涉及 Maven、容器、依赖注入、ORM,课设周期里很容易把大量时间耗在环境问题上。第三,Java 面试里最常问的 JDBC、PreparedStatement、事务,在桌面程序里全都能直接展示,答起来有据可依。

如果你明确想冲高分、或者题目要求“Web 版”,再选 Spring Boot + Thymeleaf。这时候系统会变成浏览器访问的网页,内置 Tomcat,数据库连接交给 HikariCP,SQL 用 MyBatis 管理。需要补的前置知识包括 Maven、HTTP、Session,工作量多出三分之一以上。JavaFX 的视觉效果比 Swing 好,但 FXML 和属性绑定的学习曲线不低,遇到版本兼容问题时可参考的课程设计案例也少,除非界面颜值是硬性评分点,否则不推荐在课设周期里临时换。

路线界面形态学习成本演示依赖答辩追问点
Swing桌面窗口低JDK、MySQLJDBC、事务、线程
JavaFX桌面窗口中JDK、MySQLFXML、属性绑定
Spring Boot + Thymeleaf浏览器页面中高JDK、MySQL、浏览器容器、Session、拦截器

2.2 工程包结构与启动入口

一个课设工程,包结构先定住,后面写代码才不会乱。我常用的结构是这样的:

com.station ├── frame # 窗口界面:登录窗、主窗、油品管理窗、销售窗 ├── dao # 数据访问层:OilDao、OrderDao、MemberDao ├── model # 实体类:Oil、Employee、Member、SaleOrder、SaleItem ├── util # 工具类:DbUtils、DateUtils、CsvUtils └── App.java # 启动类:设置外观,打开登录窗口

main 启动类只做一件事:初始化 Swing 外观并 new 出登录窗口,不要在里面写一堆 setVisible,否则代码会变成没人愿意读的黑匣子。实体类只放字段和 getter/setter,字段类型尽量和数据库列对应,金额用 BigDecimal,日期用 java.time.LocalDateTime。DAO 层每个方法管一条 SQL,方法名用 insertOil、updateOil、deleteOil、findByPage 这类直白命名,答辩时能少记很多事。

启动入口的写法固定如下,可以直接当作模板:

public class App { public static void main(String[] args) { try { UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { e.printStackTrace(); } new LoginFrame().setVisible(true); } }

这里把 setLookAndFeel 包在 try-catch 里是因为不同操作系统的外观类名不一样,catch 住异常后程序继续用默认外观,不影响功能。setVisible(true) 放在 new 之后,窗口对象创建时就会加载登录逻辑所需的控件。

2.3 运行环境准备:JDK、MySQL、驱动

先说 JDK。课设环境最容易翻车的是装了新版 JDK 后出现“源发行版 17 需要目标发行版 17”的编译报错,后面避坑章专门讲。大多数教材和 Java 基础题目用的还是 JDK 8 或 11,建议直接用 JDK 8 的最后一个免费版本,配合 IDEA 的 Project Structure 把 SDK 和语言级别统一,别让 IDEA 里出现两个版本号。环境变量配置这块是老生常谈,但每年都有人卡住:JAVA_HOME 不要带 bin 后缀,Path 里加 %JAVA_HOME%\bin,配置完必须重开终端。CLASSPATH 是历史包袱,用 IDEA 时不需要手动配,配错了反而找不到驱动 jar。

MySQL 用 5.7 或 8.x 都行,注意驱动类名不同:5.x 驱动用 com.mysql.jdbc.Driver,8.x 驱动用 com.mysql.cj.jdbc.Driver,且 JDBC URL 要加 serverTimezone=Asia/Shanghai。驱动 jar 如果用 Maven,pom.xml 里加依赖时 version 用 8.x 系列里和本地 MySQL 兼容的版本,我习惯以仓库实际可用版本为准;传统做法是把 jar 放进工程 lib 目录并 add as library。课设演示用传统 lib 方式最稳,少一次网络下载的变数。

环境项配置内容验证命令
JDK安装 8/11,JAVA_HOME 指向安装目录java -version
MySQL创建 station 库,编码 utf8mb4mysql -u root -p
驱动mysql-connector-java 的 jar 放进 lib 并引用Class.forName 不抛异常

数据库方面,建立库后先执行 SET NAMES utf8mb4,后面所有中文字段才不会变成问号。如果选了 Maven 路线,pom.xml 里依赖坐标长这样:

<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.x</version> </dependency>

这里的 8.x 不是占位符玩笑,是让你去看 Maven 仓库里当前能拉到的 8 系版本,不同年份的 8.x 对 MySQL 服务端版本要求略有差异。无论哪种方式,最终的验证标准只有一个:Java 代码里能成功拿到 Connection。

3. 数据库设计:五张表把油品、会员、订单和库存串起来

3.1 把加油站的业务流程拆成实体关系

设计数据库前,先把业务走一遍。员工登录系统,选择油品,输入加油升数,系统算金额;如果是会员,记录会员卡号,结账后更新会员余额;每天下班,打印当日各类油品的销量和销售额。针对这个流程,我拆成五张核心表:

  • oil 油品表:存油品种类、单价、当前库存升数。
  • employee 员工表:存账号、密码、姓名、角色。
  • member 会员表:存会员卡号、姓名、电话、余额。
  • sale_order 销售单主表:一条记录对应一次加油业务的整体信息,含员工、会员、总金额、开单时间。
  • sale_item 销售明细表:一次交易里的每种油品一行,含油品 id、单价、升数、小计金额。

主表和明细表分开,是为了能回答两个问题:一次交易买了多少种油,以及每种油卖了多少钱。只建一张订单表的话,Java 代码里就得用逗号拼接油品名,日结报表会非常难受。外键关系上,sale_order 的 emp_id 指向 employee,member_id 指向 member;sale_item 的 order_id 指向 sale_order,oil_id 指向 oil。这样从明细到油品到员工再到会员,链路是通的。

3.2 建库建表 SQL:直接可执行的版本

下面的 SQL 可以直接复制到 MySQL 里执行,注释和默认值都写在里面。

CREATE DATABASE IF NOT EXISTS station DEFAULT CHARSET utf8mb4; USE station; CREATE TABLE oil ( oil_id INT PRIMARY KEY AUTO_INCREMENT, oil_name VARCHAR(50) NOT NULL COMMENT '油品名称', price DECIMAL(10,2) NOT NULL COMMENT '零售单价', stock_litres DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '库存升数' ) COMMENT '油品表'; CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(30) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(64) NOT NULL COMMENT '密码,SHA-256后存储', emp_name VARCHAR(50) NOT NULL COMMENT '姓名', emp_role VARCHAR(20) NOT NULL DEFAULT 'cashier' COMMENT 'admin/cashier' ) COMMENT '员工表'; CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(30) NOT NULL UNIQUE COMMENT '会员卡号', real_name VARCHAR(50) NOT NULL COMMENT '姓名', phone VARCHAR(20) COMMENT '电话', balance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '账户余额' ) COMMENT '会员表'; CREATE TABLE sale_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, member_id INT, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '应收总金额', order_time DATETIME NOT NULL COMMENT '开单时间,到秒', FOREIGN KEY (emp_id) REFERENCES employee(emp_id), FOREIGN KEY (member_id) REFERENCES member(member_id) ) COMMENT '销售单主表'; CREATE TABLE sale_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, oil_id INT NOT NULL, litres DECIMAL(10,2) NOT NULL COMMENT '加油升数', price DECIMAL(10,2) NOT NULL COMMENT '成交单价快照', amount DECIMAL(10,2) NOT NULL COMMENT '小计金额', FOREIGN KEY (order_id) REFERENCES sale_order(order_id), FOREIGN KEY (oil_id) REFERENCES oil(oil_id) ) COMMENT '销售明细表';

逻辑说明:oil 表里直接放库存升数,对课设够用,如果想支持进货批次、有效期,还要再加油品入库表,这个可以放在“后续扩展”里答给老师听。sale_order 的 member_id 允许为空,因为不是每个客户都有会员卡。所有金额和数量都用 DECIMAL(10,2) 而不是 DOUBLE,这是为了避免浮点数误差,后面避坑章会展开讲。

参数说明:DECIMAL(10,2) 表示最多 10 位数字,其中小数点后保留 2 位,对加油站单价和单笔金额足够。如果你把一年累计汇总也存成 DECIMAL(10,2) 就有风险,到千万级可能溢出,报表聚合时单独用 DECIMAL(14,2) 或者在 Java 端用 BigDecimal 计算,不要依赖同一个字段算总账。order_time 用 DATETIME,保存到秒;如果要求毫秒级流水号,你可以再加一个 VARCHAR 的流水号字段,不要靠 TIMESTAMP 的小数位硬撑。

3.3 字段类型与精度设计的几个关键决定

金额和数量字段的精度设计是这个系统最容易被扣分的点。油价计算是 8.04 乘 45.6 这种场景,double 在 Java 里算出来会得到 366.62399999999997,显示出来很尴尬。MySQL 的 DECIMAL 按十进制字符串存储,计算过程不会引入二进制浮点数误差,所以建表选型阶段就堵住了这个问题。Java 实体类里对应字段用 BigDecimal,构造时用 new BigDecimal("8.04") 或 BigDecimal.valueOf(8.04),千万不要 new BigDecimal(8.04)——后者会把 double 的近似值带进精确计算里。

时间字段同样有坑。java.util.Date 配 JDBC 的 java.sql.Date 会把时分秒丢掉,日结报表按天统计时会漏掉边界数据。正确做法是 Java 端用 LocalDateTime,写入时 setTimestamp(Timestamp.valueOf(localDateTime)),读取时 getTimestamp("order_time").toLocalDateTime()。数据库端用 DATETIME,这样 2025-07-21 23:59:59 和 2025-07-22 00:00:00 分得清清楚楚。

还有一个隐蔽问题:price 字段不能拿油品表的当前单价去覆盖历史。折扣、会员价、时段促销都可能让成交价不等于油品表当前 price,所以 sale_item 里单独存 price 快照,报表算销售额时永远用成交价。有的同学图省事,报表直接 JOIN oil 拿当前价格,油价一改,历史账单全变。这是信息管理系统的原则性问题:业务数据必须可追溯。如果你走 Spring Boot + MyBatis 路线,表结构不用改,但注意 MyBatis 实体类里的金额字段要用 BigDecimal,resultMap 不要映射成 double,否则自动映射时会把坑带回来。另外,sale_item 的 order_id 上建议再建一个普通索引 idx_order_id,日结报表和订单查询都是拿 order_id 过滤和分组,没有索引时数据量到几万条就开始慢。

4. 核心功能实现:登录、开单、库存扣减和日结报表

4.1 数据库连接工具类:别让每个 DAO 都写 DriverManager

项目里最基础也最影响后续代码质量的,是数据库连接工具类。不要在每个 DAO 方法里重复写 DriverManager.getConnection,那样事务没法做,连接也容易漏关。用 ThreadLocal 把连接绑定到当前线程,事务方法需要拿同一个连接时直接复用。

public class DbUtils { private static final String URL; private static final String USER; private static final String PASSWORD; private static ThreadLocal<Connection> connHolder = new ThreadLocal<>(); static { try (InputStream in = DbUtils.class.getResourceAsStream("/jdbc.properties")) { Properties p = new Properties(); p.load(in); URL = p.getProperty("jdbc.url"); USER = p.getProperty("jdbc.user"); PASSWORD = p.getProperty("jdbc.password"); Class.forName(p.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { Connection conn = connHolder.get(); if (conn == null) { conn = DriverManager.getConnection(URL, USER, PASSWORD); connHolder.set(conn); } return conn; } public static void closeAll(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }

对应的 jdbc.properties 放在 src/main/resources 下:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/station?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.user=root jdbc.password=你的密码

逻辑说明:静态块只在类加载时执行一次,Class.forName 完成驱动注册。ThreadLocal 保证同一个线程里的多个 DAO 方法拿到同一个 Connection,事务方法里才能让多条 SQL 共享一个连接,回滚时才能把整个事务撤销。closeAll 接收 AutoCloseable 可变参数,统一关闭 ResultSet、PreparedStatement、Connection,减少 finally 里的样板代码。

参数说明:characterEncoding=utf8 是中文不乱码的前提;serverTimezone=Asia/Shanghai 是 MySQL 8.x 驱动必填项,不填会报 CST 时区错误;useSSL=false 只是避免本机演示时的 SSL 警告。密码不要硬编码在 Java 类里,放到 properties 文件中并加进 .gitignore,既是好习惯,也防止拷工程时把数据库密码带到公用电脑上。

4.2 登录与权限:PreparedStatement 和角色控制

登录是最能体现 Java 基础是否扎实的部分。下面这个方法处理员工登录查询,注意密码先做 SHA-256 再比对,数据库中不要存明文。

public Employee login(String empNo, String password) { String sql = "SELECT emp_id, emp_no, emp_name, emp_role FROM employee " + "WHERE emp_no=? AND password=?"; try (Connection conn = DbUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, empNo); ps.setString(2, sha256(password)); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Employee e = new Employee(); e.setEmpId(rs.getInt("emp_id")); e.setEmpNo(rs.getString("emp_no")); e.setEmpName(rs.getString("emp_name")); e.setEmpRole(rs.getString("emp_role")); return e; } } } catch (SQLException e) { throw new RuntimeException("登录查询失败", e); } return null; }

逻辑说明:PreparedStatement 的占位符 ? 会把输入当参数传给数据库,不参与 SQL 语法解析,这是防 SQL 注入的底线。Java 八股文里常说的“拼接 SQL 导致注入”,本质是用户输入变成了 SQL 结构;只要用 PreparedStatement,单引号、注释符都会被当普通字符串处理。sha256 方法用 MessageDigest 实现,把密码变成 64 位十六进制串。

权限控制这里先做简单方案:返回的 Employee 对象里带 empRole,主窗口根据角色决定哪些菜单可见。管理员看到“油品管理”和“报表”,收银员只看到“开单”和“会员查询”,JMenu 的 setVisible 一行就能控制。如果是 Spring Boot 版本,就换成交互器拦截 Session 里的 user 对象,没有登录跳回登录页,思路完全一致。

4.3 油品管理:PreparedStatement 完成更新

油品管理本质是单表增删改查,挑更新方法做例子,其他的照模板改 SQL 就行。

public boolean updateOil(Oil oil) { String sql = "UPDATE oil SET oil_name=?, price=?, stock_litres=? WHERE oil_id=?"; try (Connection conn = DbUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, oil.getOilName()); ps.setBigDecimal(2, oil.getPrice()); ps.setBigDecimal(3, oil.getStockLitres()); ps.setInt(4, oil.getOilId()); return ps.executeUpdate() == 1; } catch (SQLException e) { throw new RuntimeException("更新油品失败", e); } }

逻辑说明:setBigDecimal 对应数据库的 DECIMAL(10,2) 列,这样油价从 Java 到 MySQL 全程都是精确十进制。executeUpdate 返回受影响行数,更新成功是 1;如果 SQL 有语法错误或违反约束,抛 SQLException,由窗口层 catch 住弹提示框。

删除油品的逻辑要单独注意:sale_item 表通过外键引用 oil_id,一个油品只要在历史订单里出现过,直接 delete 会触发外键异常。所以删除按钮要先执行一条 SELECT COUNT(*) FROM sale_item WHERE oil_id=? 检查,有记录就把该油品的状态改成下架而不是物理删除。这个细节在答辩时很加分,说明你理解了外键约束不是负担,而是数据完整的保险。

4.4 销售开单与库存扣减:事务保证数据一致性

这是整个系统最核心的一段代码,也是 Java 面试里“怎么保证数据一致性”的实战答案。开单涉及四件事:插入主表、插入明细、扣减库存、如果失败全部回滚。下面代码把前两步放进同一个事务方法中。

public int createOrder(Connection conn, SaleOrder order, List<SaleItem> items) throws SQLException { String insertOrder = "INSERT INTO sale_order(emp_id, member_id, total_amount, order_time) " + "VALUES (?,?,?,?)"; String insertItem = "INSERT INTO sale_item(order_id, oil_id, litres, price, amount) " + "VALUES (?,?,?,?,?)"; String deduct = "UPDATE oil SET stock_litres = stock_litres - ? " + "WHERE oil_id = ? AND stock_litres >= ?"; PreparedStatement psOrder = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setInt(1, order.getEmpId()); psOrder.setObject(2, order.getMemberId()); psOrder.setBigDecimal(3, order.getTotalAmount()); psOrder.setTimestamp(4, Timestamp.valueOf(order.getOrderTime())); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); keys.next(); int orderId = keys.getInt(1); for (SaleItem item : items) { try (PreparedStatement psItem = conn.prepareStatement(insertItem)) { psItem.setInt(1, orderId); psItem.setInt(2, item.getOilId()); psItem.setBigDecimal(3, item.getLitres()); psItem.setBigDecimal(4, item.getPrice()); psItem.setBigDecimal(5, item.getAmount()); psItem.executeUpdate(); } try (PreparedStatement psDeduct = conn.prepareStatement(deduct)) { psDeduct.setBigDecimal(1, item.getLitres()); psDeduct.setInt(2, item.getOilId()); psDeduct.setBigDecimal(3, item.getLitres()); if (psDeduct.executeUpdate() == 0) { throw new SQLException("库存不足或油品不存在"); } } } return orderId; }

调用方代码如下,负责开启事务、失败回滚和最后关闭连接:

Connection conn = null; try { conn = DbUtils.getConnection(); conn.setAutoCommit(false); int orderId = orderDao.createOrder(conn, order, items); conn.commit(); return orderId; } catch (Exception e) { if (conn != null) { conn.rollback(); } throw new RuntimeException("开单失败,已回滚", e); } finally { DbUtils.closeAll(conn); }

逻辑说明:插入主表后用 getGeneratedKeys 拿自增主键,再插入明细,扣库存放在明细操作之后。扣库存的 UPDATE 写成 stock_litres >= ? 是防超卖的关键:MySQL InnoDB 对这条 UPDATE 加行锁,同一时刻两个线程并发扣同一油品时,只有一个能成功,另一个 affected rows 为 0,于是抛异常回滚整单。这比先 SELECT 判断再 UPDATE 更安全,判断和扣减在一条语句里原子完成。

参数说明:setAutoCommit(false) 之后,只有 commit 才真正落库;rollback 之前,前面插的主表和明细都会被撤销,不会出现“单子开了但库存没扣”或者反过来。finally 里 closeAll 只关闭连接,因为 PreparedStatement 已经在各自的 try-with-resources 里关掉了。注意 setObject(2, null) 用于 member_id 为空的情况,如果用 setInt 传 null 会触发类型错误。

4.5 日结报表与导出 CSV:聚合查询和文件输出

日结报表用聚合查询,一次性把当天每种油品的销量和销售额取出来。下面按自然日统计,边界条件写清楚了再不会漏单。

public List<DailyReportRow> dailyReport(LocalDate date) { String sql = "SELECT o.oil_name, SUM(i.litres) AS litres, SUM(i.amount) AS amount " + "FROM sale_item i " + "JOIN oil o ON i.oil_id = o.oil_id " + "JOIN sale_order so ON i.order_id = so.order_id " + "WHERE so.order_time >= ? AND so.order_time < ? " + "GROUP BY o.oil_id, o.oil_name"; try (Connection conn = DbUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setTimestamp(1, Timestamp.valueOf(date.atStartOfDay())); ps.setTimestamp(2, Timestamp.valueOf(date.plusDays(1).atStartOfDay())); try (ResultSet rs = ps.executeQuery()) { List<DailyReportRow> rows = new ArrayList<>(); while (rs.next()) { DailyReportRow row = new DailyReportRow(); row.setOilName(rs.getString("oil_name")); row.setLitres(rs.getBigDecimal("litres")); row.setAmount(rs.getBigDecimal("amount")); rows.add(row); } return rows; } } catch (SQLException e) { throw new RuntimeException("日结报表查询失败", e); } }

导出 CSV 时,不要直接写字符串,要处理文件编码和 Excel 的兼容性:

public void exportCsv(List<DailyReportRow> rows, LocalDate date, Path output) throws IOException { try (BufferedWriter w = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) { w.write("\uFEFF日期,油品,销量(升),销售额(元)\n"); for (DailyReportRow row : rows) { w.write(date + "," + row.getOilName() + "," + row.getLitres() + "," + row.getAmount() + "\n"); } } }

逻辑说明:查询用 >= 当天 0 点和 < 明天 0 点两个边界,而不是 BETWEEN ... AND 23:59:59,避免下一毫秒的数据被边界漏掉。GROUP BY 后 SUM 的是 DECIMAL 列,结果在 Java 端用 getBigDecimal 拿,全程不会出现浮点误差。导出 CSV 时先写一个 BOM 头(\uFEFF),Excel 打开 UTF-8 文件时中文表头才不会乱码,这个细节能让打印效果干净很多。

参数说明:如果报表还要按员工统计业绩、按会员统计消费次数,只需要改 WHERE 条件和 GROUP BY 字段,聚合规则完全一样。CSV 里的日期建议用 ISO 格式 2025-07-21,不要写 07/21/2025,后者在 Excel 里会被识别成自定义格式,不同地区显示不同。

5. 避坑与排查:这些坑不处理,答辩现场只能干瞪眼

5.1 现象:界面上中文全是问号,导出 CSV 用 Excel 打开乱码

原因:数据库连接 URL 没带 characterEncoding=utf8,或者建库时字符集不是 utf8mb4;CSV 写入时用了系统默认编码。MySQL 8.x 客户端连接默认字符集可能为 latin1,中文入库时就变问号。

解决:建库时指定 DEFAULT CHARSET utf8mb4;JDBC URL 增加 useUnicode=true&characterEncoding=utf8;CSV 导出时手动写 UTF-8 BOM。还要检查 IDEA 的 File Encoding,把 Project Encoding 和 Global Encoding 都改成 UTF-8。如果 IDE 控制台打印中文乱码,Run Configuration 的 VM options 加 -Dfile.encoding=UTF-8。

5.2 现象:Class.forName 报 ClassNotFoundException 或 Communications link failure

原因:驱动 jar 没放对位置;MySQL 驱动版本和数据库版本不匹配;数据库服务没启动;URL 里少了 serverTimezone。

解决:先确认 lib 目录的 jar 被 IDE 正确引用,再确认 Class.forName 的类名和 jar 匹配。5.x 驱动类名是 com.mysql.jdbc.Driver,8.x 是 com.mysql.cj.jdbc.Driver,把 8.x jar 配成 5.x 类名必然报错。连接 8.x 数据库时 URL 要带 serverTimezone=Asia/Shanghai,否则很快就抛时区异常。本地调试时先命令行执行 mysql -u root -p,命令行能进、程序连不上,基本是 URL 用户或密码写错。每次改驱动配置后,记得 Maven Reload 或者重新构建工程,别让 IDE 缓存继续跑旧 classpath。

5.3 现象:油价 45.6 乘 8.04 显示 366.62399999999997

原因:用了 double 或 float 存储金额。二进制浮点数无法精确表示十进制小数,这是 Java 基础里最经典的问题。更隐蔽的是 new BigDecimal(8.04) 也会出错,因为构造参数在传入之前已经是 double 近似值。

解决:金额字段统一用 DECIMAL,Java 实体用 BigDecimal。构造时用 new BigDecimal("8.04") 字符串构造器,或 BigDecimal.valueOf(8.04)。运算时用 add、multiply,除法必须指定小数位和舍入模式,比如 setScale(2, RoundingMode.HALF_UP)。报表汇总全程 BigDecimal,最后 toString 输出。这个坑不填,演示时输入 0.1 加 0.2 显示 0.30000000000000004,老师一眼看穿。

5.4 现象:两个收银员同时开单,库存变成负数

原因:先查库存再判断再扣减,两个线程同时通过判断,然后各自扣减,库存就超扣了。单机 Swing 程序也会发生,多个窗口或后台线程同时调用同一个 DAO 就有竞态,这就是并发问题。

解决:扣库存 UPDATE 直接带上库存足够条件,一条 SQL 同时完成检查和扣减:

UPDATE oil SET stock_litres = stock_litres - ? WHERE oil_id = ? AND stock_litres >= ?

受影响行数为 0 就抛异常回滚。整个开单包在事务里,setAutoCommit(false),失败 rollback。MySQL InnoDB 的行锁保证同一时刻只有一个事务能更新同一行,所以这条 UPDATE 是原子操作,比先 SELECT FOR UPDATE 更简洁,也比 synchronized 锁住 JVM 更可靠。这个回答在很多 Java 面试里都能直接用作“数据一致性”的案例。

5.5 现象:IDEA 编译报“源发行版 17 需要目标发行版 17”

原因:IDEA 的 Project SDK、Project language level、Modules language level、Java Compiler settings 四处版本不一致。比如 SDK 配了 17,language level 还指着 8;或者 Maven 的 compiler 插件 source/target 是 17,但本地 JDK 是 8。

解决:把 SDK 和 language level 统一。File -> Project Structure -> Project,SDK 选 JDK 8 或 11,Language level 对应到同一版本;Modules 里每个模块的 language level 也要改,只改 Project 不够。如果用 Maven,在 pom.xml 的 properties 里加上 maven.compiler.source 和 maven.compiler.target,再 Reload。下载 JDK 时注意和 IDEA 指定版本一致,不要在官网下完 21 然后工程跑 8 的语言级别。这个问题是 java 环境变量配置错误的常见变种,排查时全项目搜索 <maven.compiler.source> 确认没有旧值即可。

6. 最后一步:把课设从“能跑”抬到“能答辩”

6.1 三个自测场景

功能全跑通以后,建议按三个场景完整过一遍。场景一:新员工能否登录,管理员能看到油品管理菜单而收银员看不到;场景二:某油品库存只剩 5 升,强行买 10 升,系统是否弹错误并回滚整单,订单表里不能出现半截数据;场景三:日结报表和手工按小票统计的金额一致,差异超过 0.01 元就说明有金额没有走 BigDecimal。这三个场景覆盖登录权限、事务回滚、精度核算三条主线,比自己随机点按钮有效得多。

6.2 答辩前值得补的验证技巧

一个低成本技巧是在 DAO 层做一个调试开关,用布尔变量控制每次 SQL 执行前把 SQL 和参数打印到控制台,答辩前关掉即可。遇到空指针时,看控制台最后一条输出就知道是第几步查不到数据。另一个技巧是手工制造一条坏数据,比如把 sale_item 的 oil_id 改成不存在的 id,验证外键能挡住,挡住后程序是否给出用户友好提示而不是一长串堆栈。这能证明你不仅写了正向流程,还考虑了容错。

我自己的习惯是交前一晚只做一件事:把数据库删掉,用那份干净的建表脚本从零建一遍库,再全流程跑一次。不少同学开发时数据库里残留了手工插入的测试数据,程序才正常;老师现场建库一跑就崩。干净脚本能跑通,才说明系统不是依赖手工数据活着。把这个习惯坚持住,比多写一个按钮更能让老师信服。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:18:34

安路FPGA上部署Cortex-M0软核完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:17:28

C# UDP通信实战包:解决Send无响应、Receive卡死、Wireshark抓不到包

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:17:03

2022年408真题解析:DMA与外存磁道扇区计算综合题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:16:10

深度学习图像分类实战:垃圾分类识别工程从训练到部署全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:15:18

中兴W101D2刷机教程:晶晨S905L3A盒子变全功能电视盒子

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华