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