:三角色權(quán)限隔離與訂單狀態(tài)機實戰(zhàn))
簡介本資源是一套完整的智慧鄉(xiāng)村旅游服務(wù)平臺畢業(yè)設(shè)計項目面向計算機專業(yè)本科生及Java全棧初學(xué)者聚焦鄉(xiāng)村旅游數(shù)字化管理場景提供從需求分析、系統(tǒng)開發(fā)到部署演示的全流程實踐范例。壓縮包共895個文件40.48MB涵蓋128個Vue前端頁面組件、119個Java后端業(yè)務(wù)邏輯與控制器類、103個JS交互腳本、177個PNG/SVG界面圖標及3個MP4視頻演示文件完整支撐小程序SSMMySQL技術(shù)棧的學(xué)習(xí)與復(fù)現(xiàn)另有SQL建表語句、畢業(yè)論文文檔及多套批處理腳本如install.bat/run.bat便于快速搭建本地環(huán)境。目前已有313人學(xué)習(xí)下載資源包含管理員、用戶、商家三端功能模塊支持景點管理、路線規(guī)劃、訂單交易與充值等核心業(yè)務(wù)結(jié)構(gòu)清晰、注釋規(guī)范適合作為課程設(shè)計參考、畢設(shè)開題原型或微信小程序Java后端整合開發(fā)的實戰(zhàn)訓(xùn)練材料。1. 這不是又一個“旅游小程序Demo”它是一套能跑通真實角色流轉(zhuǎn)、帶完整后臺管理閉環(huán)的畢業(yè)級全棧工程你可能已經(jīng)點開過十幾個標著“鄉(xiāng)村旅游”“微信小程序”的畢業(yè)設(shè)計資源點進去發(fā)現(xiàn)只有前端頁面輪播圖幾個空接口或者后端連登錄校驗都寫在 Controller 里硬編碼。但這次不一樣——這個源碼包里真有三個角色管理員/用戶/商家在 MySQL 里各自走完注冊、權(quán)限隔離、訂單狀態(tài)機、收藏聯(lián)動、充值流水等完整業(yè)務(wù)鏈路SSM 層不是只貼了 Spring Boot 啟動類就收工而是把 MyBatis 的動態(tài) SQL 寫到了OrderMapper.xml里用choose處理多條件查詢Controller 層真做了Valid校驗 全局異常處理器微信小程序端不是拿wx.request硬懟 URL而是封裝了api/index.js統(tǒng)一攔截 token 過期并跳轉(zhuǎn)登錄頁。它不追求高并發(fā)或微服務(wù)但每一步都踩在 SSM 小程序開發(fā)的真實協(xié)作邊界上比如商家上傳景點圖片時后端用MultipartFile接收存到本地upload/目錄再把相對路徑寫進數(shù)據(jù)庫小程序端則用wx.uploadFile配合wx.chooseImage完成閉環(huán)。如果你正卡在“畢設(shè)怎么讓三個角色數(shù)據(jù)不串”“SSM 怎么和小程序 token 對齊”“MySQL 表字段命名怎么兼顧可讀性和外鍵約束”這份資源就是為你拆解過的、能直接git clone跑起來的參照系。2. 從零啟動三步拉起完整環(huán)境看清前后端如何握手2.1 后端部署SSM 工程結(jié)構(gòu)解析與關(guān)鍵配置項定位這個項目采用標準 Maven 結(jié)構(gòu)根目錄下pom.xml明確聲明了 Spring 4.3.28.RELEASE SpringMVC 4.3.28.RELEASE MyBatis 3.4.6 MySQL Connector/J 5.1.47。注意不要升級 Spring 到 5.x否則Controller類中ModelAndView返回邏輯會因視圖解析器變更而報 404這是血淚經(jīng)驗。核心配置集中在src/main/resources下applicationContext.xml定義了數(shù)據(jù)源DruidDataSource、事務(wù)管理器DataSourceTransactionManager、MyBatis 工廠SqlSessionFactoryBean及掃描路徑com.example.**.mapperspring-mvc.xml啟用了注解驅(qū)動mvc:annotation-driven /配置了靜態(tài)資源映射/static/**→classpath:/static/并指定了視圖解析器前綴/WEB-INF/jsp/、后綴.jspjdbc.properties包含jdbc.urljdbc:mysql://localhost:3306/tourism?useUnicodetruecharacterEncodingutf8——必須確保你的 MySQL 服務(wù)已啟用utf8mb4字符集否則景點名稱中的 emoji 或生僻字會變問號提示webapp/WEB-INF/web.xml中contextConfigLocation指向applicationContext.xml而DispatcherServlet的contextConfigLocation指向spring-mvc.xml這是 SSM 分層加載的關(guān)鍵。若啟動時報No mapping found for HTTP request先檢查此處是否錯配。2.2 數(shù)據(jù)庫初始化建庫、建表、填初始數(shù)據(jù)三連擊項目提供tourism.sql位于根目錄或doc/子目錄執(zhí)行前需手動創(chuàng)建數(shù)據(jù)庫CREATE DATABASE tourism CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后執(zhí)行 SQL 文件。重點觀察以下 5 張核心表結(jié)構(gòu)表名關(guān)鍵字段說明業(yè)務(wù)意義usersroletinyint0用戶,1商家,2管理員,statustinyint0禁用,1啟用角色與狀態(tài)分離避免用字符串判別scenic_spotstype_id外鍵關(guān)聯(lián)spot_types,route_id外鍵關(guān)聯(lián)spot_routes,is_hottinyint景點與類型、路線強關(guān)聯(lián)支持首頁熱榜篩選ordersuser_id,spot_id,order_status0待支付,1已支付,2已發(fā)貨,3已完成,4已取消,pay_time狀態(tài)機字段明確pay_time為datetime類型非timestamp避免時區(qū)自動轉(zhuǎn)換collectionsuser_id,spot_id,collect_time用戶收藏表無冗余字段靠聯(lián)合索引(user_id, spot_id)防重復(fù)system_configconfig_key如site_name,config_value如智慧鄉(xiāng)村旅游平臺全局配置表避免硬編碼執(zhí)行后用 Navicat 或命令行驗證SELECT COUNT(*) FROM users WHERE role 2; -- 應(yīng)返回至少 1 條管理員記錄賬號 admin / 密碼 123456 SELECT * FROM system_config WHERE config_key site_name;2.3 微信小程序端開發(fā)者工具配置與云開發(fā)兼容性處理項目未使用云開發(fā)所有接口均指向本地后端。打開微信開發(fā)者工具導(dǎo)入miniprogram/目錄注意不是整個項目根目錄關(guān)鍵修改在project.config.json和app.jsproject.config.json中appid字段需替換為你自己的測試號 AppID可在微信公眾平臺申請否則無法調(diào)用wx.loginapp.js中全局配置globalData包含baseUrl: http://localhost:8080/tourism/——若后端部署在其他機器需改為 IP 地址如http://192.168.1.100:8080/tourism/不能用localhostutils/api.js封裝了請求攔截wx.getStorageSync(token)讀取本地 tokenheader: { Authorization: Bearer token }發(fā)送若 token 過期fail回調(diào)中會wx.navigateTo({ url: /pages/login/login })。注意微信開發(fā)者工具默認開啟「不校驗合法域名」但真機調(diào)試時必須在「開發(fā)管理」中將http://localhost:8080加入「request 合法域名」實際生產(chǎn)需換為 HTTPS 域名。若真機報request:fail net::ERR_CONNECTION_REFUSED先確認后端 Tomcat 是否已啟動且監(jiān)聽 8080 端口。2.4 一鍵腳本實操1-install.bat、2-run.bat、3-build.bat的真相項目提供了三個.bat腳本但它們并非全自動魔法1-install.bat本質(zhì)是mvn clean install -Dmaven.test.skiptrue作用是編譯 Java 代碼、運行 MyBatis Generator若配置了、打包成tourism.war放入target/。執(zhí)行前需確保pom.xml中plugin里的mybatis-generator-maven-plugin版本與generatorConfig.xml兼容本項目用 1.3.22-run.bat調(diào)用mvn tomcat7:run啟動內(nèi)嵌 Tomcat端口 8080適合開發(fā)調(diào)試。若報Address already in use: bind改pom.xml中port為 80813-build.bat執(zhí)行mvn clean package -Dmaven.test.skiptrue生成tourism.war可部署到獨立 Tomcat。部署后訪問http://localhost:8080/tourism/admin/login.jsp即進入后臺登錄頁賬號 admin / 密碼 123456。提示.bak文件如main.css.bak是原始備份可忽略org.eclipse.wst.common.component是 Eclipse 項目配置IntelliJ 用戶無需關(guān)注。3. 角色權(quán)限落地三層角色如何通過 Filter Session SQL 實現(xiàn)數(shù)據(jù)隔離3.1 后臺權(quán)限控制基于 Session 的角色路由攔截管理員、商家、普通用戶的菜單入口完全不同系統(tǒng)未用 Shiro 或 Spring Security而是用原生FilterHttpSession實現(xiàn)輕量級控制。關(guān)鍵文件是src/main/java/com/example/filter/RoleFilter.javapublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(); // 從 session 獲取用戶信息登錄時存入 Object userObj session.getAttribute(user); if (userObj null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } User user (User) userObj; String uri req.getRequestURI(); // 根據(jù)角色放行對應(yīng)路徑 if (user.getRole() 2 uri.contains(/admin/)) { // 管理員訪問 /admin/** chain.doFilter(request, response); } else if (user.getRole() 1 uri.contains(/merchant/)) { // 商家訪問 /merchant/** chain.doFilter(request, response); } else if (user.getRole() 0 (uri.contains(/user/) || uri.contains(/index.jsp))) { // 用戶訪問 /user/** 或首頁 chain.doFilter(request, response); } else { // 角色越權(quán)重定向到無權(quán)限頁 req.setAttribute(msg, 無訪問權(quán)限); req.getRequestDispatcher(/error/noAuth.jsp).forward(req, resp); } }邏輯說明Filter在每次請求時觸發(fā)通過session.getAttribute(user)獲取登錄態(tài)再根據(jù)user.getRole()值判斷 URI 前綴是否匹配。參數(shù)說明user.getRole()是tinyint類型值 0/1/2 嚴格對應(yīng)數(shù)據(jù)庫users.role字段不可用字符串a(chǎn)dmin比較。3.2 小程序端權(quán)限分流App.js 全局路由守衛(wèi)小程序沒有傳統(tǒng) Session權(quán)限控制依賴wx.setStorageSync(token)和后端返回的role字段。app.js中onLaunch方法完成首次鑒權(quán)App({ onLaunch: function () { const token wx.getStorageSync(token); if (token) { wx.request({ url: http://localhost:8080/tourism/user/info, method: GET, header: { Authorization: Bearer token }, success: (res) { if (res.data.code 200) { const userInfo res.data.data; wx.setStorageSync(userInfo, userInfo); // 存入用戶信息含 role 字段 // 根據(jù) role 跳轉(zhuǎn)不同首頁 if (userInfo.role 0) { wx.switchTab({ url: /pages/index/index }); } else if (userInfo.role 1) { wx.switchTab({ url: /pages/merchant/home }); } else if (userInfo.role 2) { wx.navigateTo({ url: /pages/admin/login }); // 管理員走獨立登錄頁 } } } }); } } });參數(shù)說明userInfo.role由后端UserController.info()接口返回該接口從JwtUtil.parseToken(token)解析出用戶 ID再查users表獲取role字段。JWT 密鑰在JwtUtil.java中硬編碼為tourism_secret生產(chǎn)環(huán)境必須替換為隨機密鑰。3.3 數(shù)據(jù)庫層隔離同一張表如何讓不同角色看到不同數(shù)據(jù)最典型的場景是scenic_spots表管理員能看到全部景點商家只能看到自己發(fā)布的用戶只能看到status1審核通過的。后端通過動態(tài) SQL 實現(xiàn)!-- ScenicSpotMapper.xml -- select idlistByRole resultTypeScenicSpot SELECT * FROM scenic_spots where if testrole 0 !-- 用戶 -- status 1 /if if testrole 1 !-- 商家 -- merchant_id #{userId} AND status IN (1,2) !-- 商家可看自己待審核和已發(fā)布 -- /if if testrole 2 !-- 管理員 -- 11 !-- 查全部 -- /if /where ORDER BY create_time DESC /select邏輯說明MyBatis 的if標簽根據(jù)傳入的role參數(shù)動態(tài)拼接 WHERE 條件。參數(shù)說明#{userId}是商家 ID從SecurityUtil.getCurrentUserId()獲取該工具類從ThreadLocal中取當前登錄用戶 ID避免 Controller 層重復(fù)傳參。3.4 訂單狀態(tài)機從下單到完成的 5 個狀態(tài)如何被三方協(xié)同更新訂單流程是檢驗權(quán)限設(shè)計的試金石。orders表的order_status字段驅(qū)動整個生命周期狀態(tài)值觸發(fā)方觸發(fā)條件后端邏輯0待支付用戶提交訂單后OrderService.createOrder()插入記錄order_status01已支付用戶支付成功回調(diào)PayNotifyController.notify()校驗簽名后update order_status1, pay_timenow()2已發(fā)貨商家點擊“發(fā)貨”按鈕MerchantOrderController.ship()校驗role1且order_status1更新order_status2, ship_timenow()3已完成用戶確認收貨UserOrderController.confirm()校驗role0且order_status2更新order_status3, finish_timenow()4已取消用戶/商家/管理員任一角色調(diào)用取消接口OrderController.cancel()根據(jù)角色判斷用戶僅能取消order_status0商家可取消0/1管理員可取消全部關(guān)鍵約束OrderService.cancelOrder()方法中對商家取消操作加了雙重校驗if (order.getStatus() ! 0 order.getStatus() ! 1) { throw new ServiceException(商家只能取消待支付或已支付的訂單); }4. 避坑指南那些讓你調(diào)試到凌晨三點的典型問題與解法4.1 現(xiàn)象小程序登錄后wx.request返回 401但后端日志顯示 token 解析成功原因微信小程序wx.request默認不攜帶 Cookie而本項目后端JwtUtil從request.getHeader(Authorization)讀取 token但小程序端header中未設(shè)置Authorization字段。解決檢查utils/api.js中request方法確認header是否包含Authorization。常見錯誤是wx.getStorageSync(token)返回undefined因登錄接口未正確存儲應(yīng)在login.js的success回調(diào)中顯式寫入wx.setStorageSync(token, res.data.data.token); wx.setStorageSync(userInfo, res.data.data.user);4.2 現(xiàn)象Tomcat 啟動后訪問http://localhost:8080/tourism報 404但http://localhost:8080/tourism/login.jsp可訪問原因web.xml中welcome-file-list未配置index.jsp或index.jsp不在webapp/根目錄下。本項目index.jsp位于webapp/但web.xml缺少歡迎頁配置。解決在webapp/WEB-INF/web.xml的web-app標簽下添加welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list4.3 現(xiàn)象商家上傳景點圖片后頁面顯示空白數(shù)據(jù)庫scenic_spots.image_path字段存的是upload/20230501/abc.jpg但瀏覽器訪問http://localhost:8080/tourism/upload/20230501/abc.jpg報 404原因Tomcat 默認不提供upload/目錄的靜態(tài)資源服務(wù)需在web.xml中配置DefaultServlet映射。解決在webapp/WEB-INF/web.xml中添加servlet-mapping servlet-namedefault/servlet-name url-pattern/upload/*/url-pattern /servlet-mapping并確認scenic_spots.image_path存儲的是相對路徑不含upload/前綴前端拼接時用http://localhost:8080/tourism/upload/image_path。4.4 現(xiàn)象管理員后臺點擊“刪除景點”瀏覽器彈出確認框后頁面無反應(yīng)F12 查看 Network 發(fā)現(xiàn)DELETE請求返回 500原因ScenicSpotController.delete()方法中scenicSpotService.removeById(id)調(diào)用前未校驗景點是否已被訂單引用導(dǎo)致外鍵約束失敗拋出SQLIntegrityConstraintViolationException。解決在ScenicSpotService.removeById()中增加前置檢查// 先查是否有訂單關(guān)聯(lián)此景點 Long orderCount orderMapper.countBySpotId(id); if (orderCount 0) { throw new ServiceException(該景點已有訂單無法刪除); }4.5 現(xiàn)象MySQL 執(zhí)行tourism.sql時scenic_spots表創(chuàng)建失敗報錯Specified key was too long; max key length is 767 bytes原因scenic_spots.name字段為varchar(200)使用utf8mb4字符集時單字符最多占 4 字節(jié)200×4800 767。解決修改tourism.sql中scenic_spots.name字段長度為varchar(191)191×4764 ≤ 767或在 MySQL 5.7 中執(zhí)行SET GLOBAL innodb_file_formatBarracuda; SET GLOBAL innodb_file_per_tableON; SET GLOBAL innodb_large_prefixON; ALTER TABLE scenic_spots ROW_FORMATDYNAMIC;5. 真實業(yè)務(wù)縫合如何把“景點路線”模塊從靜態(tài)列表變成可交互的導(dǎo)航閉環(huán)5.1 路線數(shù)據(jù)結(jié)構(gòu)設(shè)計為什么用spot_routes表而非 JSON 字段很多初學(xué)者會把路線點存成JSON字符串如{points:[{lat:30.1,lng:120.2,name:入口},{lat:30.2,lng:120.3,name:觀景臺}]}但這會導(dǎo)致無法按坐標范圍查詢、無法建立空間索引。本項目采用關(guān)系型設(shè)計CREATE TABLE spot_routes ( id BIGINT PRIMARY KEY AUTO_INCREMENT, route_name VARCHAR(100) NOT NULL, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE route_points ( id BIGINT PRIMARY KEY AUTO_INCREMENT, route_id BIGINT NOT NULL, point_name VARCHAR(50) NOT NULL, latitude DECIMAL(10,8) NOT NULL, -- 緯度 longitude DECIMAL(11,8) NOT NULL, -- 經(jīng)度 sort_order INT NOT NULL DEFAULT 0, -- 排序序號 FOREIGN KEY (route_id) REFERENCES spot_routes(id) ON DELETE CASCADE );優(yōu)勢route_points表可對latitude/longitude建復(fù)合索引支持“查找距離某坐標 1km 內(nèi)的所有路線點”sort_order字段保證前端渲染順序ON DELETE CASCADE確保刪除路線時自動清理點數(shù)據(jù)。5.2 小程序端路線展示Canvas 繪制折線 Marker 標注的實戰(zhàn)代碼pages/route/detail.js中onLoad獲取路線數(shù)據(jù)后調(diào)用drawRoute()方法drawRoute(points) { const query wx.createSelectorQuery(); query.select(#routeCanvas).fields({ node: true, size: true }).exec((res) { const canvas res[0].node; const ctx canvas.getContext(2d); const dpr wx.getSystemInfoSync().pixelRatio; canvas.width res[0].width * dpr; canvas.height res[0].height * dpr; ctx.scale(dpr, dpr); // 計算畫布坐標將經(jīng)緯度映射到畫布像素 const bounds this.getBounds(points); const scaleX res[0].width / (bounds.maxLng - bounds.minLng); const scaleY res[0].height / (bounds.maxLat - bounds.minLat); // 繪制折線 ctx.beginPath(); ctx.setStrokeStyle(#1890ff); ctx.setLineWidth(3); points.forEach((point, index) { const x (point.longitude - bounds.minLng) * scaleX; const y res[0].height - (point.latitude - bounds.minLat) * scaleY; // Y軸翻轉(zhuǎn) if (index 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); // 繪制 Marker points.forEach((point, index) { const x (point.longitude - bounds.minLng) * scaleX; const y res[0].height - (point.latitude - bounds.minLat) * scaleY; ctx.beginPath(); ctx.arc(x, y, 6, 0, 2 * Math.PI); ctx.setFillStyle(index 0 ? #52c418 : index points.length - 1 ? #faad14 : #1890ff); ctx.fill(); ctx.setFontSize(12); ctx.setFillStyle(#000); ctx.setTextAlign(center); ctx.setTextBaseline(middle); ctx.fillText(point.point_name, x, y - 15); }); }); }邏輯說明getBounds()計算所有點的經(jīng)緯度極值用于歸一化scaleX/scaleY將地理坐標轉(zhuǎn)為畫布像素Y 軸翻轉(zhuǎn)是因為地圖緯度北增南減而 Canvas Y 軸下增上減。參數(shù)說明dpr設(shè)備像素比確保高清屏顯示清晰ctx.arc()繪制圓形 Marker起點綠色、終點橙色、中間藍色符合用戶認知。5.3 后端路線推薦基于用戶歷史訂單的協(xié)同過濾簡易實現(xiàn)當用戶查看某景點時后端SpotController.recommendRoutes()接口會返回“常與該景點一同被訂購的路線”。不用復(fù)雜算法用 SQL 關(guān)聯(lián)即可SELECT sr.id, sr.route_name, COUNT(*) as freq FROM spot_routes sr JOIN route_points rp ON sr.id rp.route_id JOIN orders o ON rp.point_name IN ( SELECT ss.name FROM scenic_spots ss WHERE ss.id o.spot_id ) WHERE o.user_id #{userId} AND o.order_status 3 -- 已完成訂單 GROUP BY sr.id, sr.route_name ORDER BY freq DESC LIMIT 3;說明該 SQL 通過route_points.point_name關(guān)聯(lián)scenic_spots.name找出用戶歷史訂單中景點名稱匹配的路線點再按路線分組計數(shù)。雖簡單但比純隨機推薦更貼近真實行為。5.4 從“能跑”到“能用”我每次交付前必做的三件事第一強制走一遍「用戶下單→商家發(fā)貨→用戶確認→管理員查看統(tǒng)計」全流程用 Postman 模擬每個角色的請求頭特別是Authorization驗證orders.order_status字段是否按預(yù)期流轉(zhuǎn)第二導(dǎo)出tourism數(shù)據(jù)庫用mysqldump --skip-triggers --no-create-info tourism data_only.sql清除建表語句只留INSERT作為交付給導(dǎo)師的“純凈數(shù)據(jù)”第三在小程序project.config.json中把appid改為空字符串這樣即使別人拿到代碼也無法直接上線倒逼他們理解配置項含義。希望幫到你。本文還有配套的精品資源點擊獲取