:城市印象系統(tǒng)從選型到答辯全攻略)
簡介這份資源是面向具備Java與Web開發(fā)基礎(chǔ)的學(xué)生、開發(fā)者及項目管理者的期末項目設(shè)計文檔圍繞「城市印象系統(tǒng)」展開幫助讀者完成從需求分析到系統(tǒng)實現(xiàn)的完整開發(fā)流程。文檔以MVC模式規(guī)劃了城市概覽、地圖導(dǎo)航、景點(diǎn)推薦、文化體驗、旅游指南、用戶評論評分、實時天氣、社交分享、旅行日志、線下活動與特色產(chǎn)品購買等模塊并給出數(shù)據(jù)庫設(shè)計與設(shè)計報告格式規(guī)范適合練習(xí)架構(gòu)設(shè)計與團(tuán)隊協(xié)作。壓縮包內(nèi)共1個docx文件約132KB集中承載功能選擇、框架結(jié)構(gòu)、數(shù)據(jù)庫保存與界面美化等要求便于直接參考撰寫設(shè)計報告。目前已有181人學(xué)習(xí)讀者可據(jù)此掌握J(rèn)ava Web開發(fā)方法、MVC落地思路與項目分工管理并對照格式要求完成規(guī)范文檔。1. 城市印象系統(tǒng)一個Java Web期末項目到底要落地哪些東西每年到了期末總有一批同學(xué)被“基于Java的Web程序設(shè)計”這門課的大作業(yè)卡住。題目聽起來很文藝——城市印象系統(tǒng)但真打開IDE那一刻腦子里全是問號這玩意兒到底要做成什么樣是做個城市介紹頁面還是要帶后臺管理數(shù)據(jù)庫幾張表前端要不要框架答辯的時候老師會問什么我?guī)У膸讓脤W(xué)生里做這個題目的翻車率其實不低。不是因為技術(shù)有多難而是因為大多數(shù)人一開始就沒想清楚“城市印象”這四個字在技術(shù)層面到底對應(yīng)什么。說白了它就是一個以城市為主題的內(nèi)容展示與管理系統(tǒng)前臺讓用戶瀏覽城市信息、看圖文介紹、可能還要打分或留言后臺讓管理員增刪改查城市數(shù)據(jù)、管理用戶評論。技術(shù)棧通常就是Servlet/JSP或者Spring Boot加MyBatis再加MySQL前端用HTML/CSS/JS或者套個Bootstrap。適合誰適合正在學(xué)Java Web、需要在一個學(xué)期內(nèi)交出可運(yùn)行、可演示、代碼量可控的期末項目的同學(xué)。這篇文章就按我實際帶項目的路徑把選型、建表、編碼、排錯、答辯要點(diǎn)全部拆開講讓你少走彎路。2. 技術(shù)選型與數(shù)據(jù)庫設(shè)計別一上來就堆框架2.1 Servlet/JSP還是Spring Boot期末項目怎么選這是第一個分叉口。很多同學(xué)聽說Spring Boot“高級”上來就搞結(jié)果光是配依賴和解決版本沖突就耗掉一周。我的建議很直接看你課程教的是什么。如果課程主要講Servlet、JSP、JDBC那就老老實實用ServletJSPJDBC老師看的是你對請求響應(yīng)流程的理解不是你會不會用注解。如果課程后半段講了Spring MVC或Spring Boot那用Spring BootMyBatis更省事配置少、啟動快。具體判斷標(biāo)準(zhǔn)可以看這張表對比項ServletJSPJDBCSpring BootMyBatis學(xué)習(xí)成本低課程通常已覆蓋中需要理解IoC和Mapper代碼量較大每個功能要寫Servlet較小ControllerServiceMapper調(diào)試難度低流程直觀中出錯時堆棧較深答辯加分體現(xiàn)基礎(chǔ)扎實體現(xiàn)工程化意識適合場景課程只講到Servlet課程涉及Spring我一般會讓學(xué)生先花半天時間把兩種方案的最小可運(yùn)行Demo各跑一個哪個跑通了用哪個。不要在這個環(huán)節(jié)糾結(jié)超過一天期末項目的時間要留給業(yè)務(wù)邏輯。2.2 城市印象系統(tǒng)的數(shù)據(jù)庫表怎么設(shè)計數(shù)據(jù)庫是整個系統(tǒng)的地基。城市印象系統(tǒng)最少需要四張表城市表、城市分類表、用戶表、評論表。如果還要做圖片輪播再加一張城市圖片表。下面是我常用的建表SQL直接可以在MySQL 5.7或8.0里執(zhí)行-- 城市分類表比如“歷史名城”“海濱城市”“美食之都” CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分類名稱, sort_order INT DEFAULT 0 COMMENT 排序權(quán)重 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 城市表核心數(shù)據(jù) CREATE TABLE city ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 城市名稱, category_id INT COMMENT 所屬分類, description TEXT COMMENT 城市簡介, cover_img VARCHAR(255) COMMENT 封面圖路徑, view_count INT DEFAULT 0 COMMENT 瀏覽量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用戶表簡單起見只存用戶名密碼 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL COMMENT 存MD5值別存明文, role TINYINT DEFAULT 0 COMMENT 0普通用戶 1管理員 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 評論表用戶對城市的留言 CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, city_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (city_id) REFERENCES city(id), FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這里有幾個參數(shù)要說明。字符集用utf8mb4而不是utf8因為城市簡介里可能出現(xiàn)生僻字或emoji。外鍵約束在期末項目里建議加上答辯時老師看到外鍵會認(rèn)為你理解關(guān)系型數(shù)據(jù)庫的完整性。view_count字段用來做簡單的熱度排序不需要額外建表。password字段長度給100是為了存MD5加密后的32位字符串留余量。2.3 項目目錄結(jié)構(gòu)怎么組織才不亂不管用哪種技術(shù)棧目錄結(jié)構(gòu)要清晰。Servlet方案下我一般這樣分src/ com.cityimpression.dao/ -- 數(shù)據(jù)庫操作類 com.cityimpression.entity/ -- 實體類和表對應(yīng) com.cityimpression.service/ -- 業(yè)務(wù)邏輯 com.cityimpression.servlet/ -- 控制器 com.cityimpression.util/ -- 工具類如DBUtil、MD5Util WebContent/ css/ js/ images/ WEB-INF/web.xml index.jsp cityList.jsp cityDetail.jsp login.jsp admin/Spring Boot方案下更簡單按controller、service、mapper、entity分包即可。關(guān)鍵原則是一個功能模塊的代碼放在一起不要按技術(shù)層散落到各處。比如“城市管理”相關(guān)的Servlet、Service、DAO都放在city子包下找起來快答辯演示時也方便講。3. 核心功能編碼從城市列表到評論提交的完整鏈路3.1 用JDBC工具類打通數(shù)據(jù)庫連接不管后面用什么框架理解JDBC連接池是基礎(chǔ)。期末項目里不需要上Druid或HikariCP手寫一個簡單的DBUtil就夠。下面這個工具類負(fù)責(zé)加載驅(qū)動、獲取連接、關(guān)閉資源package com.cityimpression.util; import java.sql.*; public class DBUtil { // 數(shù)據(jù)庫配置改成你自己的庫名和密碼 private static final String URL jdbc:mysql://localhost:3306/city_impression?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD your_password; static { try { // MySQL 8.0驅(qū)動類名是com.mysql.cj.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(數(shù)據(jù)庫驅(qū)動加載失敗, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 關(guān)閉資源順序不能錯ResultSet - Statement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }邏輯說明靜態(tài)代碼塊保證驅(qū)動只加載一次。URL里的serverTimezone參數(shù)在MySQL 8.0下必須加否則會報時區(qū)錯誤這是血淚經(jīng)驗。characterEncoding設(shè)為utf8mb4防止中文亂碼。close方法的關(guān)閉順序不能顛倒先關(guān)結(jié)果集再關(guān)語句再關(guān)連接否則可能拋異常。參數(shù)怎么改URL中的city_impression換成你的數(shù)據(jù)庫名USER和PASSWORD換成你的MySQL賬號。如果MySQL是5.7版本驅(qū)動類名可以保留com.mysql.jdbc.Driver但建議統(tǒng)一用8.0驅(qū)動。3.2 城市列表分頁查詢的Servlet實現(xiàn)城市列表是前臺最核心的頁面。數(shù)據(jù)量不大時可以直接全查但為了體現(xiàn)分頁思想我建議加上分頁。下面是一個CityListServlet的核心代碼package com.cityimpression.servlet; import com.cityimpression.entity.City; import com.cityimpression.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.List; WebServlet(/cityList) public class CityListServlet extends HttpServlet { private static final int PAGE_SIZE 6; // 每頁顯示6條 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 獲取當(dāng)前頁碼默認(rèn)第1頁 int page 1; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { try { page Integer.parseInt(pageStr); } catch (NumberFormatException ignored) {} } if (page 1) page 1; ListCity cityList new ArrayList(); int totalCount 0; String countSql SELECT COUNT(*) FROM city; String listSql SELECT c.*, cat.name AS categoryName FROM city c LEFT JOIN category cat ON c.category_id cat.id ORDER BY c.view_count DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection()) { // 先查總數(shù) try (PreparedStatement ps conn.prepareStatement(countSql); ResultSet rs ps.executeQuery()) { if (rs.next()) totalCount rs.getInt(1); } // 再查當(dāng)前頁數(shù)據(jù) try (PreparedStatement ps conn.prepareStatement(listSql)) { ps.setInt(1, PAGE_SIZE); ps.setInt(2, (page - 1) * PAGE_SIZE); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { City city new City(); city.setId(rs.getInt(id)); city.setName(rs.getString(name)); city.setDescription(rs.getString(description)); city.setCoverImg(rs.getString(cover_img)); city.setViewCount(rs.getInt(view_count)); city.setCategoryName(rs.getString(categoryName)); cityList.add(city); } } } } catch (SQLException e) { e.printStackTrace(); req.setAttribute(error, 數(shù)據(jù)查詢失敗請稍后重試); } int totalPages (int) Math.ceil((double) totalCount / PAGE_SIZE); req.setAttribute(cityList, cityList); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/cityList.jsp).forward(req, resp); } }邏輯說明先查總數(shù)再查當(dāng)前頁這是分頁的標(biāo)準(zhǔn)做法。LIMIT和OFFSET的參數(shù)用PreparedStatement設(shè)置防止SQL注入。LEFT JOIN分類表是為了在列表頁顯示分類名稱避免在JSP里再查一次。try-with-resources語法自動關(guān)閉資源比手動close更安全。參數(shù)說明PAGE_SIZE設(shè)為6是考慮到頁面布局每行3個卡片、共2行。page參數(shù)從URL獲取比如cityList?page2。如果頁碼超出范圍可以在JSP里做判斷也可以在這里加邊界檢查。3.3 評論提交與防重復(fù)刷屏評論功能看似簡單但有兩個坑一是沒登錄也能提交二是同一用戶短時間重復(fù)提交。下面這段代碼在插入前做了登錄校驗和時間間隔檢查WebServlet(/addComment) public class AddCommentServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 1. 登錄校驗 Object userObj req.getSession().getAttribute(user); if (userObj null) { resp.sendRedirect(login.jsp?msg請先登錄); return; } int userId ((User) userObj).getId(); int cityId Integer.parseInt(req.getParameter(cityId)); String content req.getParameter(content); // 2. 內(nèi)容非空和長度校驗 if (content null || content.trim().isEmpty() || content.length() 500) { resp.sendRedirect(cityDetail?id cityId msg評論內(nèi)容不合法); return; } // 3. 防重復(fù)檢查該用戶對該城市最近一條評論的時間 String checkSql SELECT create_time FROM comment WHERE user_id? AND city_id? ORDER BY create_time DESC LIMIT 1; String insertSql INSERT INTO comment(city_id, user_id, content) VALUES(?,?,?); try (Connection conn DBUtil.getConnection()) { try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, userId); ps.setInt(2, cityId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { long lastTime rs.getTimestamp(create_time).getTime(); // 30秒內(nèi)不允許重復(fù)評論 if (System.currentTimeMillis() - lastTime 30_000) { resp.sendRedirect(cityDetail?id cityId msg評論太頻繁); return; } } } } try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, cityId); ps.setInt(2, userId); ps.setString(3, content.trim()); ps.executeUpdate(); } } catch (SQLException e) { e.printStackTrace(); } resp.sendRedirect(cityDetail?id cityId); } }邏輯說明登錄校驗放在最前面未登錄直接跳轉(zhuǎn)登錄頁。內(nèi)容校驗包括非空和長度上限。防重復(fù)用“查最近一條評論時間”的方式30秒內(nèi)不允許再評。這個閾值可以根據(jù)演示需要調(diào)整比如改成10秒。參數(shù)說明30_000是毫秒數(shù)代表30秒。content長度限制500和數(shù)據(jù)庫字段一致。cityId從表單隱藏域傳入。4. 避坑與排查那些讓項目跑不起來的常見問題4.1 中文亂碼從請求到響應(yīng)到數(shù)據(jù)庫的三層排查現(xiàn)象頁面顯示的城市名是“??¤′”這樣的亂碼或者提交的評論存進(jìn)數(shù)據(jù)庫變成問號。原因亂碼可能出現(xiàn)在三個環(huán)節(jié)——請求編碼、響應(yīng)編碼、數(shù)據(jù)庫連接編碼。很多人只改了JSP的pageEncoding忽略了POST請求體和數(shù)據(jù)庫連接串。解決第一JSP文件頭部確保是% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。第二在Servlet的doPost第一行加req.setCharacterEncoding(UTF-8)doGet在Tomcat 8以后默認(rèn)UTF-8但保險起見也加。第三數(shù)據(jù)庫連接URL里加characterEncodingutf8mb4。第四建表時指定DEFAULT CHARSETutf8mb4。四層都對齊亂碼基本消失。4.2 數(shù)據(jù)庫連接報“Public Key Retrieval is not allowed”現(xiàn)象MySQL 8.0下啟動項目報錯java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。原因MySQL 8.0默認(rèn)使用caching_sha2_password認(rèn)證插件JDBC連接時如果不允許公鑰檢索就會失敗。解決在連接URL后面加allowPublicKeyRetrievaltrue。完整URL類似jdbc:mysql://localhost:3306/city_impression?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrue。這個參數(shù)只影響本地開發(fā)答辯演示時沒問題。4.3 圖片上傳后訪問404現(xiàn)象后臺添加城市時上傳了封面圖數(shù)據(jù)庫里存了路徑但前臺img標(biāo)簽加載不出來。原因圖片存到了項目外的目錄或者存到了WebContent下但重啟后丟失或者路徑寫的是絕對路徑而Tomcat部署后路徑變了。解決期末項目最簡單的做法是把圖片存到WebContent/images/city/目錄下數(shù)據(jù)庫存相對路徑如images/city/beijing.jpgJSP里用${pageContext.request.contextPath}/images/city/beijing.jpg拼接。如果存到項目外需要在Tomcat里配置虛擬路徑映射但期末項目不建議這么復(fù)雜。另外注意上傳目錄要有寫權(quán)限。4.4 分頁查詢頁碼越界導(dǎo)致空列表現(xiàn)象在地址欄手動輸入cityList?page999頁面顯示空白但沒有報錯。原因OFFSET計算后超出了總記錄數(shù)SQL返回空結(jié)果集JSP遍歷時沒有數(shù)據(jù)。解決在Servlet里計算totalPages后如果page大于totalPages把page重置為totalPages?;蛘咴贘SP里判斷currentPage totalPages時顯示“沒有更多數(shù)據(jù)”。更優(yōu)雅的做法是在SQL層用LIMIT ? OFFSET ?之前先校驗page范圍。4.5 答辯時被問“你的系統(tǒng)有什么技術(shù)難點(diǎn)”怎么答現(xiàn)象演示很流暢但老師一問“你覺得難點(diǎn)在哪”就卡殼。原因只關(guān)注了功能實現(xiàn)沒有提煉技術(shù)問題。解決提前準(zhǔn)備三個點(diǎn)。第一分頁查詢的SQL優(yōu)化說明為什么用LIMIT而不是全查再截取。第二防SQL注入說明PreparedStatement的原理。第三會話管理說明Session和Cookie的區(qū)別以及登錄狀態(tài)怎么保持。每個點(diǎn)準(zhǔn)備一句話解釋加一句代碼位置答辯時直接翻到對應(yīng)代碼。5. 進(jìn)階技巧讓城市印象系統(tǒng)在答辯時多拿幾分5.1 用過濾器統(tǒng)一處理編碼和登錄攔截如果每個Servlet都寫一遍req.setCharacterEncoding和登錄校驗代碼重復(fù)且容易漏。用Filter可以一次性解決WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 放行登錄頁、登錄接口、靜態(tài)資源 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(req, resp); return; } // 后臺頁面需要管理員權(quán)限 if (uri.contains(/admin/)) { Object user request.getSession().getAttribute(user); if (user null || ((User) user).getRole() ! 1) { response.sendRedirect(request.getContextPath() /login.jsp); return; } } chain.doFilter(req, resp); } }邏輯說明過濾器攔截所有請求統(tǒng)一設(shè)置編碼。通過URI判斷放行靜態(tài)資源和登錄相關(guān)路徑。后臺路徑檢查Session中的用戶角色。這樣Servlet里就不用重復(fù)寫編碼和權(quán)限代碼了。參數(shù)說明WebFilter(/*)表示攔截所有路徑。放行列表根據(jù)你的項目結(jié)構(gòu)調(diào)整。角色判斷里role1代表管理員。5.2 用AJAX做無刷新評論提升演示效果答辯演示時提交評論后整頁刷新會打斷節(jié)奏。用AJAX提交評論區(qū)局部更新觀感更好。前端代碼// 提交評論 function submitComment(cityId) { var content document.getElementById(commentContent).value.trim(); if (!content) { alert(請輸入評論內(nèi)容); return; } fetch(addComment, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: cityId cityId content encodeURIComponent(content) }).then(function(resp) { if (resp.redirected) { // 被重定向說明未登錄或參數(shù)不合法 window.location.href resp.url; } else { // 刷新評論列表 loadComments(cityId); document.getElementById(commentContent).value ; } }); }邏輯說明用fetch發(fā)送POST請求Content-Type設(shè)為表單格式。encodeURIComponent防止內(nèi)容里的特殊字符截斷參數(shù)。如果后端返回重定向說明校驗未通過直接跳轉(zhuǎn)。否則重新加載評論列表并清空輸入框。參數(shù)說明cityId從頁面隱藏域或JSP變量獲取。loadComments是另一個AJAX函數(shù)負(fù)責(zé)查詢并渲染評論列表。5.3 答辯前必做的五項自檢第一換一臺電腦部署確認(rèn)數(shù)據(jù)庫腳本能完整執(zhí)行、項目能啟動。第二把所有硬編碼的數(shù)據(jù)庫密碼改成配置文件讀取至少說明你知道配置分離。第三準(zhǔn)備一份數(shù)據(jù)備份SQL演示前導(dǎo)入避免現(xiàn)場數(shù)據(jù)被改亂。第四把瀏覽器控制臺和Tomcat日志打開確認(rèn)沒有紅色報錯。第五錄一段3分鐘的演示視頻作為備份萬一現(xiàn)場環(huán)境出問題可以播放。我自己的習(xí)慣是答辯前一晚把整個流程走三遍啟動、登錄、瀏覽、評論、后臺增刪改查、退出。每遍記錄耗時和卡頓點(diǎn)。第三遍如果能在5分鐘內(nèi)走完且無報錯基本就穩(wěn)了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取