Java+Swing+MySQL构建停车场管理系统:从建表到避坑全解析
发布时间:2026/10/9 12:33:31 锦皓数字建站

简介这是一份基于JavaSwingMySQL实现的停车场管理系统完整源码面向Java初学者、课程设计与毕业设计人群涵盖车辆出入登记、车位状态管理、用户权限控制等核心业务。系统采用MVC分层思想通过JDBC连接MySQL数据库并包含事件驱动界面、预编译防SQL注入和异常处理等工程实践。资源包共79个文件含25个Java源文件、47个编译后的class文件以及SQL建表脚本、项目配置和界面预览图等整体压缩包约2.08MB。源码结构清晰可直接导入Eclipse或IDEA运行附带数据库表结构与初始数据方便快速搭建本地环境。已有2351人学习下载。通过这份代码开发者可以系统梳理Java Swing桌面应用与MySQL交互的完整流程了解车位表、车辆表、交易表之间的关联设计为独立完成同类管理系统或二次功能扩展提供可靠参考。1. 停车场管理系统都 2026 年了为什么我还在推荐 JavaSwingMysql 这套组合一个看起来像课设题目的系统实际需求一直没断过。学校要交差单位要一个管几十个车位的内部工具小商户要解决车停进来了、该收多少钱这种最朴素的问题。JavaSwingMysql 这套组合在 2026 年依然是到岸最短的路径Swing 负责桌面界面不需要前端工程化Mysql 负责数据落地和查询Java 写中间那层业务逻辑。适合谁做课设的学生、需要快速交付内部管理工具的一线工程师。反直觉的结论是对于这种单机或小并发场景这套老组合的上手速度和可维护性比 Spring Boot Vue 那套更直接——没有 Node 环境、没有跨域问题、不用考虑部署容器双击一个 jar 就能跑。2. 先立数据层Mysql 建表、JDBC 连接与 HikariCP 连接池配置2.1 停车场需要哪几张表车位、订单、会员的字段设计一个停车场管理系统最小集是四张表车位表、停车订单表、会员表、计费规则表。我习惯先把字段设计讲完因为很多教程一上来就写界面结果做到计费发现字段不够用回头改表连带改界面来回折腾两三遍。表结构才是这个项目的承重墙。-- 车位表 CREATE TABLE car_space ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 车位ID, space_no VARCHAR(10) NOT NULL UNIQUE COMMENT 车位编号如 A-01, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 2预留, area VARCHAR(20) DEFAULT NULL COMMENT 区域如 地面/负一层, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表; -- 停车订单表 CREATE TABLE parking_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 订单号程序生成, space_id INT NOT NULL COMMENT 车位ID, plate_no VARCHAR(12) NOT NULL COMMENT 车牌号, member_id INT DEFAULT NULL COMMENT 会员ID非必填, in_time DATETIME NOT NULL COMMENT 进场时间, out_time DATETIME DEFAULT NULL COMMENT 出场时间NULL表示未出场, duration_min INT NOT NULL DEFAULT 0 COMMENT 停车时长分钟, fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收费用, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在场 1已出场 2已结算, KEY idx_plate_no (plate_no), KEY idx_in_time (in_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车订单表;这段 SQL 里我故意把out_time设成可空因为车没走的时候这一行记录是不完整的。很多新手在这里踩坑把out_time设成 NOT NULL然后出场时再 INSERT 一条新记录导致一辆车产生两行历史对账对不上。状态字段status也要保留它和out_time配合避免每次查询都去判断out_time IS NULL这种索引帮不上忙的条件。会员表字段就简单很多id、name、phone、plate_no、balance、created_at。计费规则表我单独拆出来因为不同停车场的收费逻辑差异很大——有的按小时、有的按次、有的前半小时免费。把规则放表里改收费不用重新编译 Java 代码。这里有个 Mysql 查询的常见思路要提前说订单表上建了idx_in_time是因为查某时间段内的所有订单算某天营收这类统计是高频操作。如果表数据量到几十万行没有这个索引WHERE in_time BETWEEN ? AND ?就是全表扫描。在 Mysql 里用EXPLAIN看执行计划不走索引就老老实实补索引这是 Mysql 排序和过滤场景下的基本功。2.2 JDBC 连接 Mysql驱动、URL 参数与最小可运行封装设计完表下一步就要让 Java 连上 Mysql。这里最常用的组合是mysql-connector-j加 JDBC 标准接口。Maven 里这样引驱动dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency注意如果你是手动把 jar 放进项目里别用 5.x 的驱动去连 8.x 的 Mysql运行时会报 Public Key Retrieval is not allowed 或者干脆连不上。最常见的连接写法很固定我一般直接封装成一个工具类避免每个 DAO 里重复写DriverManager.getConnectionpublic class DbUtil { private static final String URL jdbc:mysql://localhost:3306/parking ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8rewriteBatchedStatementstrue; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这个类里最容易被忽略的是 URL 上的三个参数。serverTimezoneAsia/Shanghai解决的是后面避坑章节要展开的时间差 8 小时问题characterEncodingutf8保证中文字段不乱码但注意它只对连接层有效表本身的字符集还得是utf8mb4两个地方配错一个都会出问题rewriteBatchedStatementstrue只有当你打算用addBatch批量插入时才有效果几万条数据一批入库的时候这个参数能明显缩短写入耗时。如果你只是做课设数据量小最后一个参数不加也行但建议保留。Class.forName这一步在 JDBC 4.0 之后其实可以省略驱动 jar 里的META-INF/services/java.sql.Driver会被自动加载。我习惯留着是因为某些老版本 Mysql 驱动或自定义类加载环境下显式注册更稳妥而且报错时更容易定位到驱动没放进来这个原因。2.3 连接池为什么用 HikariCP 而不是每次新建 Connection连接池是很多人跳过的一步。课设规模下每次查询都DriverManager.getConnection也不是不能用但一旦上了定时刷新车位状态或者报表查询你就发现界面会时不时卡一下。原因是每次新建 Connection 都要经历 TCP 握手、Mysql 权限校验、认证这一整套流程普通机器上单次耗时可以到 50~100ms界面上的高频刷新就会累积卡顿。我一般用 HikariCP它够快、配置简单、社区活跃。配置如下HikariConfig config new HikariConfig(); config.setJdbcUrl(DbUtil.URL); config.setUsername(root); config.setPassword(你的密码); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); HikariDataSource dataSource new HikariDataSource(config);参数里MaximumPoolSize10对 Swing 桌面应用来说已经很多了因为你是单机程序并发上限就是界面上那几个按钮。ConnectionTimeout3000的意思是拿不到连接就快速失败并提示而不是让界面无限转圈——这一点对桌面端尤其重要客户端用户看到程序没反应比看到报错更慌尽早报错反而好排查。MaxLifetime设成 30 分钟是为了避开 Mysql 服务端wait_timeout默认 8 小时把连接回收掉之后客户端还拿着过期连接操作的情况。改了连接池之后所有 DAO 的写法从DbUtil.getConnection()换成从dataSource.getConnection()取外面包一层 try-with-resources。这部分的坑我放到避坑章节细说这里先记住一条原则连接池的意义不只是复用连接更是把连接的生命周期管理从业务代码里剥离出去业务逻辑不再关心连接什么时候建、什么时候关。3. Swing 界面层从 JFrame 到数据表格的搭建顺序3.1 主窗口布局用 BorderLayout 定出功能区Swing 的界面我习惯先用BorderLayout把大框架定下来不急着调像素。主窗口分三块顶部放操作按钮中间放核心数据表格底部放状态栏。这样搭的好处是后续加功能只需在对应区域插入组件不用重排。JFrame frame new JFrame(停车场管理系统); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(1024, 700); frame.setLocationRelativeTo(null); JPanel topPanel new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8)); JButton btnIn new JButton(车辆入场); JButton btnOut new JButton(车辆出场); JButton btnQuery new JButton(订单查询); JButton btnRefresh new JButton(刷新车位); topPanel.add(btnIn); topPanel.add(btnOut); topPanel.add(btnQuery); topPanel.add(btnRefresh); JTable table new JTable(); JScrollPane scrollPane new JScrollPane(table); JLabel statusLabel new JLabel(就绪); frame.add(topPanel, BorderLayout.NORTH); frame.add(scrollPane, BorderLayout.CENTER); frame.add(statusLabel, BorderLayout.SOUTH); frame.setVisible(true);BorderLayout.NORTH放按钮面板CENTER放表格SOUTH放状态栏这是 Swing 桌面程序最常规的分区。注意我用FlowLayout(FlowLayout.LEFT, 8, 8)而不是直接new FlowLayout()是为了让按钮之间有 8 像素的间距不然挤在一起。setLocationRelativeTo(null)是让窗口居中这个细节很便宜但很多新人会忘记窗口跑到屏幕角落观感很差。用了setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)后点窗口 X 号会直接退出进程。如果项目里还接了日志、未提交的订单写入这里建议改成问一句确认退出给用户留个后悔药。具体做法是设置成DO_NOTHING_ON_CLOSE然后给窗口加WindowListener在windowClosing里弹确认框确认后再System.exit(0)。3.2 JTable 与 TableModel让数据从 ResultSet 走到界面JTable 本身不存数据它靠TableModel提供行和列。最省事的是DefaultTableModel但它的缺点是列类型全是 Object排序、数值显示都要自己转。更稳的做法是写一个专门的OrderTableModel继承AbstractTableModel把数据源整理成 List。public class OrderTableModel extends AbstractTableModel { private static final String[] COLUMNS {订单号, 车牌号, 进场时间, 出场时间, 时长(分), 费用}; private ListParkingOrder orders new ArrayList(); public void setData(ListParkingOrder newOrders) { this.orders newOrders; fireTableDataChanged(); // 通知 JTable 刷新 } Override public int getRowCount() { return orders.size(); } Override public int getColumnCount() { return COLUMNS.length; } Override public String getColumnName(int column) { return COLUMNS[column]; } Override public Object getValueAt(int rowIndex, int columnIndex) { ParkingOrder o orders.get(rowIndex); switch (columnIndex) { case 0: return o.getOrderNo(); case 1: return o.getPlateNo(); case 2: return o.getInTime(); case 3: return o.getOutTime(); case 4: return o.getDurationMin(); case 5: return o.getFee(); default: return ; } } }这段代码的关键是fireTableDataChanged()。你不调用它就算 List 里数据变了界面也纹丝不动。这个方法是 TableModel 和视图之间的发布通知忘了它就是典型的数据明明变了界面不刷新的玄学问题。getColumnName重写之后列头才显示成中文不重写的话默认显示 A、B、C。从 ResultSet 到 List 的转换我建议放在 DAO 里做完DAO 只返回ListParkingOrder绝不让 ResultSet 出 DAO 层。这样 TableModel 只面对干净的实体对象界面代码不碰 JDBC 的异常处理出了问题也更好定位是 SQL 的问题还是展示的问题。3.3 事件监听按钮回调与回车提交的细节Swing 的按钮监听本质是ActionListener。这里有个细节值得注意所有耗时操作都不能直接写在actionPerformed里因为它在 EDT事件分发线程上运行。你现在做的是课设可能感觉不到一旦订单表数据过万点击查询后界面立刻卡死鼠标变成转圈就是因为在 EDT 上做了数据库查询。btnQuery.addActionListener(e - { statusLabel.setText(查询中...); new Thread(() - { try { ListParkingOrder result orderDao.queryAll(); SwingUtilities.invokeLater(() - { tableModel.setData(result); statusLabel.setText(查询完成共 result.size() 条); }); } catch (Exception ex) { SwingUtilities.invokeLater(() - statusLabel.setText(查询失败: ex.getMessage())); } }).start(); });这段代码把耗时查询放到新线程查询结果通过SwingUtilities.invokeLater再丢回 EDT 去更新界面。这是 Swing 多线程的标准写法所有对界面组件的操作只能在 EDT 上执行这行规则记住了你就避开了界面随机卡死、偶发崩溃这类最让人抓狂的坑。回车提交也是常见需求在输入框里敲回车等价于点按钮。做法是把按钮设为默认按钮frame.getRootPane().setDefaultButton(btnQuery);。如果要支持在输入框中按回车直接触发查询可以给JTextField加ActionListener按回车时actionPerformed会被自动触发。这时我一般会抽一个doQuery()方法让按钮和回车的监听共用不要复制一份查询代码不然改了一个逻辑忘了改另一个后面排查很痛苦。4. 核心业务实现停车计费、车位状态与订单查询4.1 计费规则按时段/按次/跨天这三种怎么选计费是整个系统里最容易和需求方吵起来的部分。常见的三种规则按次收费、按小时收费、按时段收费比如白天和夜间不同价。对课设和内部系统来说我建议先做小时计费 封顶这个组合它最通用也最好演示效果。public BigDecimal calculateFee(LocalDateTime inTime, LocalDateTime outTime) { long minutes ChronoUnit.MINUTES.between(inTime, outTime); if (minutes 30) { return BigDecimal.ZERO; // 前30分钟免费 } long hours (minutes 59) / 60; // 向上取整 BigDecimal feePerHour new BigDecimal(5.00); // 每小时5元 BigDecimal fee feePerHour.multiply(BigDecimal.valueOf(hours)); // 封顶24小时最多收40元 if (fee.compareTo(new BigDecimal(40)) 0) { fee new BigDecimal(40); } return fee; }这里有几个坑是新手常踩的。第一时长必须向上取整停了 1 小时 1 分钟按 2 小时收费用(minutes 59) / 60这个整数运算就能实现比调Math.ceil再转类型利落。第二费用计算必须用BigDecimal而不是 doubledouble 算钱会有 0.1 0.2 ! 0.3 这种问题做计费系统这是底线。第三ChronoUnit.MINUTES.between是 Java 8 时间 API注意别混用java.util.Date两套时间 API 混在一起类型转换的报错会让你烦到想砸键盘。跨天场景是按时段收费才需要专门处理的比如晚上 10 点到次日 6 点算夜间价。如果只做小时计费跨天其实不用特殊处理时长差算出来就行。但有个隐藏问题如果界面让用户选的进场、出场时间都是字符串解析的时候一定要带上LocalDateTime而不是LocalDate否则跨天订单的分钟差是负数费用直接算成 0。4.2 车位状态刷新从数据库轮询到事件主动刷新车位状态有两种刷新策略。第一种是轮询用一个javax.swing.Timer每 3 秒查一次数据库更新界面上的车位状态。第二种是事件主动刷新业务操作比如车辆入场完成后立刻刷新。对单机版管理系统来说轮询更省心因为人工操作界面并没有外部系统会去改数据库没有并发场景主动刷新反而容易漏。Timer timer new Timer(3000, e - { ListCarSpace list spaceDao.queryAll(); for (CarSpace space : list) { updateSpaceStatusLabel(space); } }); timer.start();注意这里的Timer是javax.swing.Timer不是java.util.Timer。javax.swing.Timer的回调在 EDT 上执行更新界面安全java.util.Timer的回调在普通线程直接改界面组件就会踩线程问题的坑。轮询间隔设置 3 秒是个折中太短了浪费数据库连接太长了用户会觉得我明明加了辆车界面半天没变。更讲究一点的做法是只在入场、出场、手动刷新这三个时机触发刷新中间的轮询去掉。我一般给系统加一个手动刷新按钮同时保留 3 秒轮询作为兜底因为人工操作时注意力在界面上轮询能兜住一些边界情况比如另一个终端也在操作数据库。如果以后要接道闸设备或者车牌识别摄像机这个多端同时写库的场景会越来越多到时候再考虑从轮询切到事件通知也不迟。4.3 订单查询与报表SQL 聚合与排序的写法订单查询的核心是按条件过滤 按时间排序。排序这里直接用 SQL 的ORDER BY最省事别把数据全查出来再在 Java 里排序。这是个很常见的坏习惯数据几万行的时候全量加载到内存再排序界面卡不说内存也吃紧。SELECT order_no, plate_no, in_time, out_time, duration_min, fee FROM parking_order WHERE in_time BETWEEN ? AND ? AND (? IS NULL OR plate_no ?) ORDER BY in_time DESC LIMIT 500;这个 SQL 里有几个值得注意的参数设计。? IS NULL OR plate_no ?这个写法支持没填车牌号就查所有但要注意IS NULL条件会让 Mysql 对plate_no那部分索引失效所以车牌号不是高频过滤条件时这个写法可以接受。LIMIT 500是很好的保护不然用户手滑把所有数据拉出来界面直接卡死这也是 Mysql 常用命令里对LIMIT的典型用法。当日营收这种统计用GROUP BY按天聚合SELECT DATE(in_time) AS day, COUNT(*) AS order_count, SUM(fee) AS total_fee FROM parking_order WHERE status 1 GROUP BY DATE(in_time) ORDER BY day DESC;SUM(fee)在 Mysql 里对DECIMAL字段返回的是整数和小数都保留的结果直接能拿来做报表金额。这里有坑如果fee列允许 NULLSUM会返回NULL而不是 0界面上就会显示空白。处理方式是在 Java 侧做判空BigDecimal totalFee rs.getBigDecimal(total_fee); if (totalFee null) { totalFee BigDecimal.ZERO; }另外WHERE status 1很关键它把还在场、未结算的订单排除出营收统计否则你算出来的数字永远偏大且对不上账。这个条件只要漏一次后面所有报表数据都会让你怀疑人生。5. 避坑SwingMysql 联调中最常见的 5 个翻车现场5.1 中文乱码URL、表结构与驱动三个环节现象界面上显示的车牌、车主姓名变成 ??? 或者方块。原因乱码不是单点问题它有三个环节。第一连接 URL 没带characterEncodingutf8第二Mysql 表结构用了latin1或utf8utf8在 Mysql 里不是真正的四字节全字符集8.0 之后建议直接用utf8mb4第三JDBC 驱动的连接属性覆盖了 JVM 默认编码。三个环节配错任何一个中文就乱。解决连接 URL 加上characterEncodingutf8建表语句统一带DEFAULT CHARSETutf8mb4。如果已经有存量表用ALTER TABLE parking_order CONVERT TO CHARACTER SET utf8mb4;补救。改完重启应用重新插入数据再查一次。顺手在入口处检查一下System.getProperty(file.encoding)确认 JVM 没被外部环境变量带偏。5.2 时区差 8 小时serverTimezone 与 DATETIME 的偏移现象界面显示的进场时间是 2026-01-05 14:00数据库里存的是 2026-01-05 06:00或者反过来。原因Mysql 的DATETIME类型本身不带时区问题出在 JDBC 驱动把DATETIME转成java.sql.Timestamp时会按 JVM 默认时区做一次转换。如果连接 URL 没指定serverTimezone驱动会用机器默认时区一旦机器或容器时区不是Asia/Shanghai就出现 8 小时偏移。解决连接 URL 固定加serverTimezoneAsia/Shanghai同时确保 Mysql 服务端的时区一致执行SHOW VARIABLES LIKE %time_zone%;如果结果是SYSTEM就追到系统时区去确认。一致了之后代码里用LocalDateTime接收时间字段尽量避免用java.util.Date去中转Date在 toString 时会用 JVM 时区再做一次转换容易二次污染。5.3 Swing 界面卡死EDT 线程上跑数据库查询现象点击查询后窗口变白、鼠标转圈过几秒甚至十几秒才恢复期间连按钮都点不了。原因ActionListener的回调跑在 EDT 线程上。EDT 负责所有界面重绘和事件分发你在里面做数据库 IO等于把整个界面线程堵死了所有窗口都无法响应。解决耗时操作放新线程更新界面用SwingUtilities.invokeLater切回 EDT第 3.3 节已经写好了样板。如果不想每次手写 Thread也可以封装一个SwingTask辅助类统一处理前后台切换。这个坑的隐蔽性在于数据量小的时候根本触发不了等报表数据多了才炸属于延迟翻车——平时看着没问题一上线就暴露。5.4 连接不释放ResultSet 没关导致的连接泄漏现象程序跑一段时间后报 Too many connections 或者连接池的活跃连接数一直涨最后数据库拒绝连接。原因Connection关了但PreparedStatement和ResultSet没关或者三者都没关。在连接池模式下更隐蔽你以为close()是关连接实际是把连接还给连接池池里的连接只要没被正确释放就会被一直占用。解决统一用 try-with-resources这是 Java 7 之后最稳的写法try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, plateNo); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理每一行 } } }ResultSet也要单独放进 try 里因为它在PreparedStatement关闭前其实不能关闭。养成这个习惯后连接泄漏基本绝迹。排查已存在的泄漏可以在连接池配置里打开leakDetectionThresholdHikariCP 支持超过阈值就打印告警日志能帮你定位到具体代码行。5.5 驱动版本不匹配ClassNotFoundException 的排查思路现象启动时报ClassNotFoundException: com.mysql.jdbc.Driver或者No suitable driver found但 jar 明明放进去了。原因Mysql Connector/J 8.x 之后驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。用老代码Class.forName(com.mysql.jdbc.Driver)配新驱动必然报错。反过来某些 5.x 的 Mysql 服务端用 8.x 的驱动连接也会因为默认认证插件从mysql_native_password换成caching_sha2_password而报错。解决确认驱动版本和服务端版本。如果都是 8.xClass.forName里写com.mysql.cj.jdbc.Driver或者直接删掉这行用 SPI 自动加载。如果服务端还是 5.7驱动用 5.1.49 或 8.0.x 都行但连接 URL 里要显式加useSSLfalse因为 5.7 的 SSL 配置容易触发警告。还有一个隐藏原因手动导 jar 时IDE 没把 jar 加进 classpath构建路径里的引用和磁盘上的文件是两回事报错之前先去 classpath 配置页确认。6. 让停车场管理系统更耐用的收尾做法6.1 操作日志与数据库备份系统能跑起来之后第一件该补的事是日志。最简单的做法是用 Java 的FileWriter把入场、出场、异常操作追加到一个文本文件里每行带时间戳。别急着上 Log4j 之类的重框架一个内部工具先保证有日志比日志格式规范更重要。第二件事是备份Mysql 自带工具就够了mysqldump -u root -p parking /backup/parking_$(date %F).sql配合系统自带的任务计划程序每天凌晨跑一次。备份这事没出事时觉得自己多此一举出了事才庆幸自己没偷懒——世界上最便宜的后悔药就是定时备份。6.2 当系统要走向多终端事务与下一步如果这套系统的使用场景从单机变成多台电脑同时操作你要开始注意事务了。比如车辆入场这个动作要同时写一条停车订单和更新车位状态两步必须在一个事务里要么都成功要么都回滚。在 JDBC 里的写法是conn.setAutoCommit(false)操作完成后conn.commit()失败则conn.rollback()。Mysql 的 InnoDB 引擎支持事务和行级锁这是你从单机走向多终端的基石。以后再往上走可以研究把收费规则表做成界面可配置、把订单导出成 Excel、或者接一个简单的车牌识别接口。Java 基础和 Mysql 查询能力如果扎实这套系统的扩展空间比想象中大得多。我早年做第一版停车管理系统时偷懒没写日志结果有个车主说多收了钱我查了半天也说不清楚当时为什么算了 8 小时——因为没有任何记录。后来老老实实加了操作日志再遇到扯皮的事翻日志就能还原现场。哪怕是做个课设也建议把日志和备份这两个习惯带上成本极低回报却可能是一次信任的挽回。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。