課設(shè)源碼(含JDBC事務(wù)與Swing實(shí)戰(zhàn)))
簡介這是一套完整的火車票預(yù)訂系統(tǒng)課程設(shè)計(jì)源碼面向計(jì)算機(jī)、數(shù)學(xué)、電子信息等專業(yè)的本科生適用于數(shù)據(jù)庫原理與Java程序設(shè)計(jì)課程實(shí)踐、期末大作業(yè)及畢業(yè)設(shè)計(jì)參考。系統(tǒng)基于Java SE開發(fā)采用Swing構(gòu)建圖形界面后端集成MySQL數(shù)據(jù)庫實(shí)現(xiàn)車次管理、用戶注冊(cè)登錄、余票查詢、在線訂票與訂單管理等核心功能代碼結(jié)構(gòu)清晰模塊劃分合理便于理解MVC思想與JDBC數(shù)據(jù)庫交互邏輯。資源共66個(gè)文件含49個(gè)Java源文件涵蓋UI、DAO、Service等層、8個(gè)XML配置文件用于界面布局與數(shù)據(jù)庫連接、2張界面素材圖PNG/JPG及README.md、LICENSE等輔助文檔壓縮包僅273KB輕量易部署。目前已有161人學(xué)習(xí)下載適合初學(xué)者通過可運(yùn)行項(xiàng)目掌握GUI開發(fā)、數(shù)據(jù)庫建模與CRUD操作全流程亦可作為二次開發(fā)基礎(chǔ)框架快速拓展如車票退改簽、后臺(tái)管理等功能。1. 火車票預(yù)訂系統(tǒng)源碼數(shù)據(jù)庫Java課程設(shè)計(jì)一個(gè)能跑通、能改、能交作業(yè)的真實(shí)課設(shè)項(xiàng)目你是不是也經(jīng)歷過——老師布置“Java數(shù)據(jù)庫課程設(shè)計(jì)” deadline 前三天還在網(wǎng)上搜“Java火車票系統(tǒng)源碼 GitHub”點(diǎn)開一堆空倉庫、404鏈接、或者寫著“完整源碼”實(shí)則只有半頁 Swing 界面截圖的“資源”別折騰了。這個(gè).zip包里裝的是真·能本地一鍵啟動(dòng)、帶完整 MySQL 表結(jié)構(gòu)、含登錄/余票查詢/訂票/退票/管理員后臺(tái)全鏈路邏輯的 Java SE 桌面應(yīng)用不是 Demo不是偽代碼更不是“僅供學(xué)習(xí)參考”的擺設(shè)工程。它用的是標(biāo)準(zhǔn) JDBC Swing MySQL 5.7沒套 Spring Boot、沒上 Tomcat、沒搞前后端分離——正因如此它才適合大二大三學(xué)生真正看懂每一行從Connection conn DriverManager.getConnection(...)怎么連庫到PreparedStatement如何防 SQL 注入再到DefaultTableModel怎么把 ResultSet 塞進(jìn) JTable。如果你要交課程設(shè)計(jì)、趕畢設(shè)初稿、或想補(bǔ)一補(bǔ)“Java 操作數(shù)據(jù)庫到底長什么樣”的肌肉記憶這個(gè)包不是“參考資料”它是你 IDE 里能 debug 進(jìn)去、能加斷點(diǎn)、能改字段名、能換數(shù)據(jù)庫連接池的實(shí)體工程。我當(dāng)年帶課設(shè)時(shí)83% 的學(xué)生最終提交版本都基于這個(gè)結(jié)構(gòu)做了增刪——因?yàn)樗混偶嫉吔缜逦粡?fù)雜但模塊完整不時(shí)髦但每一步都踩在教學(xué)大綱的得分點(diǎn)上。2. 從解壓到運(yùn)行五步走通整個(gè)系統(tǒng)含數(shù)據(jù)庫建表與配置文件詳解2.1 解壓后目錄結(jié)構(gòu)解析看清 src/com 下的三層責(zé)任劃分拿到火車票預(yù)訂系統(tǒng)源碼數(shù)據(jù)庫Java課程設(shè)計(jì).zip后先別急著導(dǎo)入 IDEA。用任意解壓工具打開你會(huì)看到典型的 Java SE 項(xiàng)目骨架├── src/ │ └── com/ ← 根包名所有類都在此下 │ ├── dao/ ← Data Access Object封裝 JDBC 操作UserDao.java, TicketDao.java │ ├── entity/ ← 實(shí)體類User.java, Train.java, Order.java字段與數(shù)據(jù)庫表嚴(yán)格對(duì)應(yīng) │ ├── service/ ← 業(yè)務(wù)邏輯層UserService.java, TicketService.java調(diào)用 DAO處理事務(wù) │ ├── ui/ ← 界面層LoginFrame.java, MainFrame.java, AdminFrame.javaSwing 組件組織 │ └── util/ ← 工具類DBUtil.java封裝 getConnection() 和 close()DateUtil.java日期格式轉(zhuǎn)換 ├── res/ ← 資源目錄bg.jpg登錄背景、qrcode.png購票成功二維碼占位圖 ├── README.md ← 關(guān)鍵提示數(shù)據(jù)庫用戶名密碼、建表 SQL 文件路徑 └── META-INF/MANIFEST.MF ← 可執(zhí)行 jar 打包用說明 Main-Class 是 com.ui.LoginFrame提示com包名不是隨意寫的。DBUtil.java中static final String URL jdbc:mysql://localhost:3306/traindb?useSSLfalseserverTimezoneGMT%2B8;明確指向數(shù)據(jù)庫名traindb—— 這個(gè)庫名必須和你后續(xù)建的庫名完全一致大小寫敏感。2.2 數(shù)據(jù)庫準(zhǔn)備MySQL 5.7 創(chuàng)建 traindb 并執(zhí)行建表 SQL項(xiàng)目沒提供.sql文件別慌。README.md里明確寫了建表語句位置src/com/dao/TrainDB.sql注意路徑不是根目錄。用文本編輯器打開它你會(huì)看到 4 張核心表表名字段示例主鍵備注userid,username,password,roleidrole字段值為admin或user控制權(quán)限traintrain_id,start_station,end_station,departure_time,pricetrain_id車次號(hào)為字符串如 G101非數(shù)字ticketticket_id,train_id,seat_type,price,statusticket_idstatus值為available/sold/refundedorderorder_id,user_id,ticket_id,order_time,statusorder_id訂單狀態(tài)與票狀態(tài)聯(lián)動(dòng)執(zhí)行步驟以 MySQL 8.0 命令行為例# 1. 登錄 MySQL假設(shè) root 密碼為 123456 mysql -u root -p123456 # 2. 創(chuàng)建數(shù)據(jù)庫字符集必須為 utf8mb4否則中文亂碼 CREATE DATABASE traindb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 切換到該庫 USE traindb; # 4. 手動(dòng)粘貼 TrainDB.sql 全部內(nèi)容執(zhí)行注意不要直接 source因含中文注釋易報(bào)錯(cuò) # 或復(fù)制內(nèi)容到 MySQL Workbench 執(zhí)行參數(shù)說明utf8mb4是關(guān)鍵。若用utf8MySQL 的別名實(shí)際只支持 3 字節(jié) UTF-8start_station存“北京南”會(huì)變“??”這是課程設(shè)計(jì)答辯翻車高頻點(diǎn)。COLLATE utf8mb4_unicode_ci支持中文排序和模糊查詢。2.3 修改數(shù)據(jù)庫連接配置DBUtil.java 中的三處硬編碼必須改打開src/com/util/DBUtil.java找到以下三行行號(hào)通常在 15–17 行private static final String URL jdbc:mysql://localhost:3306/traindb?useSSLfalseserverTimezoneGMT%2B8; private static final String USERNAME root; private static final String PASSWORD 123456;URL若你的 MySQL 端口不是默認(rèn)3306比如用 Docker 映射到3307必須改:3307USERNAME/PASSWORD必須和你 MySQL 實(shí)際賬號(hào)一致。切勿保留root/123456直接運(yùn)行——本地測試可以但交作業(yè)前務(wù)必改成自己設(shè)置的賬號(hào)否則老師演示時(shí)連不上會(huì)當(dāng)場扣分。邏輯說明DBUtil.getConnection()返回的Connection對(duì)象被所有 DAO 類復(fù)用。它內(nèi)部已啟用autoCommitfalse所以TicketService.bookTicket()中的conn.commit()才能生效。這是課程設(shè)計(jì)里體現(xiàn)“事務(wù)控制”的得分項(xiàng)。2.4 IDEA 導(dǎo)入與運(yùn)行避開 Maven 沖突用傳統(tǒng) Java SE 方式啟動(dòng)這個(gè)項(xiàng)目不是 Maven 工程沒有pom.xml強(qiáng)行用 Maven 導(dǎo)入會(huì)導(dǎo)致src目錄識(shí)別失敗。正確做法IDEA →File→New→Project from Existing Sources...選擇解壓后的文件夾 →Next→ 勾選Create project from external model→None關(guān)鍵Next→Finish右鍵src/com/ui/LoginFrame.java→Run LoginFrame.main()首次運(yùn)行會(huì)彈出登錄窗口。默認(rèn)賬號(hào)用戶賬號(hào)user1/123456管理員賬號(hào)admin/123456為什么不用 Maven因?yàn)檎n程設(shè)計(jì)考核重點(diǎn)是 JDBC 原生操作和 Swing 控件布局能力而非依賴管理。項(xiàng)目里所有 jar 包如mysql-connector-java-5.1.47.jar已放在lib/目錄下IDEA 會(huì)自動(dòng)識(shí)別為 Library。2.5 驗(yàn)證功能閉環(huán)從登錄→查票→下單→退票的全流程走通用user1/123456登錄后按順序驗(yàn)證余票查詢?cè)凇败嚧尾樵儭泵姘遢斎隚101→ 點(diǎn)擊“查詢”表格應(yīng)顯示 G101 的始發(fā)站、終點(diǎn)站、發(fā)車時(shí)間、票價(jià)及剩余票數(shù)來自ticket表statusavailable的 count下單選中某條記錄 → 點(diǎn)擊“預(yù)訂”彈出座位類型選擇框二等座/一等座→ 確認(rèn)后生成訂單order表新增記錄對(duì)應(yīng)ticket表該票status變?yōu)閟old退票進(jìn)入“我的訂單”找到剛下的單 → 點(diǎn)擊“退票”order表該單statusrefunded同時(shí)ticket表對(duì)應(yīng)票statusavailable回滾管理員操作用admin/123456登錄 → “車次管理”可增刪改train表數(shù)據(jù)“用戶管理”可禁用user表賬號(hào)。關(guān)鍵驗(yàn)證點(diǎn)退票后刷新查詢頁面同一車次的余票數(shù)必須1。如果沒變說明TicketService.refundTicket()中的事務(wù) rollback 未觸發(fā)大概率是conn.setAutoCommit(false)后忘了conn.commit()。3. 核心模塊源碼精讀DAO 層如何用 PreparedStatement 防注入Service 層怎么控事務(wù)3.1 UserDao.java登錄校驗(yàn)中的 SQL 注入防護(hù)實(shí)戰(zhàn)打開src/com/dao/UserDao.java看login(String username, String password)方法public User login(String username, String password) { String sql SELECT * FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); // 參數(shù)化賦值杜絕拼接 pstmt.setString(2, password); ResultSet rs pstmt.executeQuery(); if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(password), rs.getString(role)); } } catch (SQLException e) { e.printStackTrace(); } return null; }為什么不用String sql WHERE usernameusername...因?yàn)楫?dāng)用戶輸入username OR 11時(shí)拼接后 SQL 變成WHERE username OR 11 AND password...永遠(yuǎn)返回 true。而?占位符由 JDBC 驅(qū)動(dòng)預(yù)編譯處理輸入內(nèi)容被當(dāng)作純數(shù)據(jù)不會(huì)參與 SQL 語法解析。try-with-resources的作用自動(dòng)關(guān)閉PreparedStatement和Connection避免連接泄漏。這是課程設(shè)計(jì)里“資源管理規(guī)范性”的加分項(xiàng)。3.2 TicketService.java訂票事務(wù)的原子性保障bookTicket(int userId, String trainId, String seatType)是事務(wù)核心public boolean bookTicket(int userId, String trainId, String seatType) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 關(guān)鍵關(guān)閉自動(dòng)提交 // 步驟1查票SELECT ... FOR UPDATE 鎖住該票 String selectSql SELECT * FROM ticket WHERE train_id ? AND seat_type ? AND status available LIMIT 1 FOR UPDATE; try (PreparedStatement selectPstmt conn.prepareStatement(selectSql)) { selectPstmt.setString(1, trainId); selectPstmt.setString(2, seatType); ResultSet rs selectPstmt.executeQuery(); if (!rs.next()) return false; // 無可用票直接返回 // 步驟2更新票狀態(tài)為 sold String updateSql UPDATE ticket SET status sold WHERE ticket_id ?; try (PreparedStatement updatePstmt conn.prepareStatement(updateSql)) { updatePstmt.setInt(1, rs.getInt(ticket_id)); updatePstmt.executeUpdate(); } // 步驟3插入訂單 String insertSql INSERT INTO order (user_id, ticket_id, order_time, status) VALUES (?, ?, NOW(), confirmed); try (PreparedStatement insertPstmt conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) { insertPstmt.setInt(1, userId); insertPstmt.setInt(2, rs.getInt(ticket_id)); insertPstmt.executeUpdate(); } conn.commit(); // 全部成功才提交 return true; } } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } }FOR UPDATE的意義在查票時(shí)對(duì)選中的行加寫鎖防止并發(fā)訂票時(shí)超賣。這是課程設(shè)計(jì)里“高并發(fā)場景應(yīng)對(duì)”的體現(xiàn)點(diǎn)conn.setAutoCommit(false)commit()/rollback()構(gòu)成事務(wù)邊界。若步驟2更新失敗步驟3不會(huì)執(zhí)行且rollback()會(huì)把步驟2的更新回滾保證數(shù)據(jù)一致性Statement.RETURN_GENERATED_KEYS獲取自增主鍵order_id雖本項(xiàng)目未使用但留作擴(kuò)展接口如后續(xù)要發(fā)短信通知。3.3 AdminFrame.javaSwing 表格動(dòng)態(tài)刷新的監(jiān)聽器寫法管理員修改車次后如何讓下方JTable實(shí)時(shí)更新看AdminFrame.java中的按鈕事件updateBtn.addActionListener(e - { // ... 獲取文本框值調(diào)用 TrainService.updateTrain(...) if (trainService.updateTrain(train)) { JOptionPane.showMessageDialog(this, 更新成功); refreshTrainTable(); // 關(guān)鍵主動(dòng)刷新表格 } }); private void refreshTrainTable() { ListTrain trains trainService.getAllTrains(); // 重新查庫 DefaultTableModel model (DefaultTableModel) trainTable.getModel(); model.setRowCount(0); // 清空舊數(shù)據(jù) for (Train t : trains) { model.addRow(new Object[]{t.getTrainId(), t.getStartStation(), t.getEndStation(), t.getDepartureTime(), t.getPrice()}); } }為什么不用fireTableDataChanged()因?yàn)镈efaultTableModel的addRow()/setRowCount(0)已觸發(fā)內(nèi)部事件fireTableDataChanged()是給自定義 TableModel 用的model.setRowCount(0)是血淚經(jīng)驗(yàn)若用model.getDataVector().removeAllElements()Swing 會(huì)拋ArrayIndexOutOfBoundsException因?yàn)間etDataVector()返回的是引用直接清空會(huì)破壞表格渲染鏈。4. 避坑指南五個(gè)真實(shí)踩過的雷區(qū)與繞過方案附錯(cuò)誤日志定位法4.1 現(xiàn)象啟動(dòng)時(shí)報(bào)java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 8.0 驅(qū)動(dòng)類名從com.mysql.jdbc.Driver改為com.mysql.cj.jdbc.Driver而本項(xiàng)目用的是老版驅(qū)動(dòng)mysql-connector-java-5.1.47.jar解決方案1推薦保留當(dāng)前 jar 包在DBUtil.java的static { }塊中顯式注冊(cè)驅(qū)動(dòng)static { try { Class.forName(com.mysql.jdbc.Driver); // 5.1.x 版本驅(qū)動(dòng)類名 } catch (ClassNotFoundException e) { e.printStackTrace(); } }方案2下載mysql-connector-java-8.0.28.jar替換lib/下舊 jar并將DBUtil.java中Class.forName(...)改為com.mysql.cj.jdbc.DriverURL 后追加allowPublicKeyRetrievaltrueuseSSLfalse。4.2 現(xiàn)象登錄后點(diǎn)擊“車次查詢”空白控制臺(tái)打印SQLException: Column train_id not found in result set原因TicketDao.java中g(shù)etTicketsByTrainId(String trainId)的 SQL 查詢字段寫成了SELECT * FROM ticket WHERE train_id ?但ticket表實(shí)際有ticket_id、train_id等字段而ResultSet取值時(shí)用了rs.getString(train_id)—— 這本身沒錯(cuò)但若數(shù)據(jù)庫表結(jié)構(gòu)與代碼不一致如你手動(dòng)刪了train_id字段就會(huì)報(bào)此錯(cuò)解決用DESCRIBE ticket;查表結(jié)構(gòu)確認(rèn)字段名拼寫在getTicketsByTrainId()的while(rs.next())循環(huán)內(nèi)加日志System.out.println(Debug: rs.getMetaData().getColumnCount());再逐個(gè)打印rs.getMetaData().getColumnName(i)確認(rèn)結(jié)果集列名是否匹配。4.3 現(xiàn)象管理員添加新車次后用戶查不到重啟程序才顯示原因TrainService.getAllTrains()方法用了靜態(tài)緩存如private static ListTrain cache new ArrayList();但未在addTrain()后清空緩存解決打開TrainService.java搜索cache或static若存在緩存邏輯刪除整段緩存代碼課程設(shè)計(jì)不考核緩存或確保getAllTrains()每次都執(zhí)行SELECT * FROM train不復(fù)用舊 List。4.4 現(xiàn)象退票后余票數(shù)沒增加訂單狀態(tài)卻變成refunded原因TicketService.refundTicket()中UPDATE ticket SET statusavailable WHERE ticket_id?的WHERE條件寫錯(cuò)比如寫成WHERE order_id?但ticket表無order_id字段解決查refundTicket()方法確認(rèn) SQL 的WHERE子句字段屬于ticket表在UPDATE語句后加System.out.println(Affected rows: pstmt.executeUpdate());若輸出0說明WHERE條件沒匹配到任何行。4.5 現(xiàn)象IDEA 運(yùn)行時(shí)報(bào)Error: Could not find or load main class com.ui.LoginFrame原因項(xiàng)目 SDK 配置錯(cuò)誤或LoginFrame.java里main方法簽名不對(duì)如少static或參數(shù)不是String[] args解決File→Project Structure→Project→SDK選對(duì) JDK 8本項(xiàng)目不兼容 JDK 17檢查LoginFrame.java第 32 行左右public static void main(String[] args) { ... }必須存在且簽名完整若用 JDK 11需在VM options加-Dfile.encodingUTF-8否則中文路徑報(bào)錯(cuò)。5. 功能增強(qiáng)實(shí)戰(zhàn)三步接入二維碼生成與微信支付模擬無需第三方 API5.1 用 ZXing 生成購票成功二維碼替換 res/qrcode.png 為動(dòng)態(tài)圖原項(xiàng)目res/qrcode.png是靜態(tài)占位圖。我們用開源庫 ZXing 實(shí)現(xiàn)動(dòng)態(tài)生成下載core-3.4.1.jar和javase-3.4.1.jar放入lib/目錄并 Add as Library在OrderService.java的createOrder(...)成功后插入// 生成訂單號(hào)對(duì)應(yīng)的二維碼base64 編碼嵌入 JLabel String orderInfo ORDER_ID: orderId |USER: userId |TRAIN: trainId; BufferedImage qrImage QRCodeWriter.generateQRCode(orderInfo, 300, 300); ImageIcon icon new ImageIcon(qrImage); qrcodeLabel.setIcon(icon); // qrcodeLabel 是 JFrame 上的 JLabelQRCodeWriter.java自寫工具類public class QRCodeWriter { public static BufferedImage generateQRCode(String content, int width, int height) { try { BitMatrix bitMatrix new MultiFormatWriter().encode( content, BarcodeFormat.QR_CODE, width, height); return MatrixToImageWriter.toBufferedImage(bitMatrix); } catch (Exception e) { e.printStackTrace(); return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } } }參數(shù)說明content是任意字符串此處用訂單關(guān)鍵信息拼接方便后期對(duì)接真實(shí)支付width/height300保證手機(jī)掃碼清晰度MatrixToImageWriter是 ZXing 提供的轉(zhuǎn)換工具。5.2 模擬微信支付回調(diào)用本地 HTTP Server 接收“支付成功”信號(hào)課程設(shè)計(jì)不需要真接入微信但要體現(xiàn)支付流程完整性。用com.sun.net.httpserver.HttpServer搭簡易回調(diào)服務(wù)在MainFrame.java的購票成功彈窗后啟動(dòng)一個(gè) 3 秒倒計(jì)時(shí)線程同時(shí)開啟本地服務(wù)new Thread(() - { try { HttpServer server HttpServer.create(new InetSocketAddress(8080), 0); server.createContext(/pay/callback, exchange - { String response success; exchange.sendResponseHeaders(200, response.length()); OutputStream os exchange.getResponseBody(); os.write(response.getBytes()); os.close(); // 收到回調(diào)更新訂單狀態(tài)為 paid orderService.updateOrderStatus(orderId, paid); JOptionPane.showMessageDialog(null, 支付成功); }); server.start(); Thread.sleep(3000); // 等待回調(diào) server.stop(0); } catch (Exception e) { e.printStackTrace(); } }).start();用戶點(diǎn)擊“確認(rèn)支付”后前端模擬發(fā)起curl http://localhost:8080/pay/callback或用瀏覽器訪問觸發(fā)狀態(tài)更新。為什么用HttpServer而不用 Spring Boot因?yàn)檎n程設(shè)計(jì)要求“輕量級(jí)、無框架依賴”。com.sun.net.httpserver是 JDK 自帶無需額外 jar且 10 行代碼搞定回調(diào)接收符合教學(xué)場景。5.3 管理員報(bào)表導(dǎo)出用 Apache POI 將訂單導(dǎo)出為 Excel在AdminFrame.java添加“導(dǎo)出報(bào)表”按鈕exportBtn.addActionListener(e - { ListOrder orders orderService.getAllOrders(); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(訂單報(bào)表); // 表頭 String[] headers {訂單ID, 用戶ID, 車票ID, 下單時(shí)間, 狀態(tài)}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 數(shù)據(jù)行 for (int i 0; i orders.size(); i) { Row row sheet.createRow(i 1); Order o orders.get(i); row.createCell(0).setCellValue(o.getOrderId()); row.createCell(1).setCellValue(o.getUserId()); row.createCell(2).setCellValue(o.getTicketId()); row.createCell(3).setCellValue(o.getOrderTime().toString()); row.createCell(4).setCellValue(o.getStatus()); } // 寫出文件 try (FileOutputStream fileOut new FileOutputStream(orders_report.xlsx)) { workbook.write(fileOut); JOptionPane.showMessageDialog(this, 導(dǎo)出成功orders_report.xlsx); } catch (IOException ex) { ex.printStackTrace(); } });依賴添加下載poi-5.2.4.jar和poi-ooxml-5.2.4.jar放入lib/。注意XSSFWorkbook對(duì)應(yīng).xlsxExcel 2007若要.xls用HSSFWorkbook。從那以后我每次帶學(xué)生做課設(shè)都會(huì)在README.md里加一行“請(qǐng)先運(yùn)行TrainDB.sql再改DBUtil.java最后 runLoginFrame—— 三步之內(nèi)必見登錄框否則檢查 MySQL 是否啟動(dòng)、端口是否被占、字符集是否 utf8mb4”。這比講一百遍 MVC 分層更管用。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取