ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

SSM+微信小程序全栈毕设:三角色权限隔离与订单状态机实战

SSM+微信小程序全栈毕设:三角色权限隔离与订单状态机实战 简介本资源是一套完整的智慧乡村旅游服务平台毕业设计项目面向计算机专业本科生及Java全栈初学者聚焦乡村旅游数字化管理场景提供从需求分析、系统开发到部署演示的全流程实践范例。压缩包共895个文件40.48MB涵盖128个Vue前端页面组件、119个Java后端业务逻辑与控制器类、103个JS交互脚本、177个PNG/SVG界面图标及3个MP4视频演示文件完整支撑小程序SSMMySQL技术栈的学习与复现另有SQL建表语句、毕业论文文档及多套批处理脚本如install.bat/run.bat便于快速搭建本地环境。目前已有313人学习下载资源包含管理员、用户、商家三端功能模块支持景点管理、路线规划、订单交易与充值等核心业务结构清晰、注释规范适合作为课程设计参考、毕设开题原型或微信小程序Java后端整合开发的实战训练材料。1. 这不是又一个“旅游小程序Demo”它是一套能跑通真实角色流转、带完整后台管理闭环的毕业级全栈工程你可能已经点开过十几个标着“乡村旅游”“微信小程序”的毕业设计资源点进去发现只有前端页面轮播图几个空接口或者后端连登录校验都写在 Controller 里硬编码。但这次不一样——这个源码包里真有三个角色管理员/用户/商家在 MySQL 里各自走完注册、权限隔离、订单状态机、收藏联动、充值流水等完整业务链路SSM 层不是只贴了 Spring Boot 启动类就收工而是把 MyBatis 的动态 SQL 写到了OrderMapper.xml里用choose处理多条件查询Controller 层真做了Valid校验 全局异常处理器微信小程序端不是拿wx.request硬怼 URL而是封装了api/index.js统一拦截 token 过期并跳转登录页。它不追求高并发或微服务但每一步都踩在 SSM 小程序开发的真实协作边界上比如商家上传景点图片时后端用MultipartFile接收存到本地upload/目录再把相对路径写进数据库小程序端则用wx.uploadFile配合wx.chooseImage完成闭环。如果你正卡在“毕设怎么让三个角色数据不串”“SSM 怎么和小程序 token 对齐”“MySQL 表字段命名怎么兼顾可读性和外键约束”这份资源就是为你拆解过的、能直接git clone跑起来的参照系。2. 从零启动三步拉起完整环境看清前后端如何握手2.1 后端部署SSM 工程结构解析与关键配置项定位这个项目采用标准 Maven 结构根目录下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这是血泪经验。核心配置集中在src/main/resources下applicationContext.xml定义了数据源DruidDataSource、事务管理器DataSourceTransactionManager、MyBatis 工厂SqlSessionFactoryBean及扫描路径com.example.**.mapperspring-mvc.xml启用了注解驱动mvc:annotation-driven /配置了静态资源映射/static/**→classpath:/static/并指定了视图解析器前缀/WEB-INF/jsp/、后缀.jspjdbc.properties包含jdbc.urljdbc:mysql://localhost:3306/tourism?useUnicodetruecharacterEncodingutf8——必须确保你的 MySQL 服务已启用utf8mb4字符集否则景点名称中的 emoji 或生僻字会变问号提示webapp/WEB-INF/web.xml中contextConfigLocation指向applicationContext.xml而DispatcherServlet的contextConfigLocation指向spring-mvc.xml这是 SSM 分层加载的关键。若启动时报No mapping found for HTTP request先检查此处是否错配。2.2 数据库初始化建库、建表、填初始数据三连击项目提供tourism.sql位于根目录或doc/子目录执行前需手动创建数据库CREATE DATABASE tourism CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后执行 SQL 文件。重点观察以下 5 张核心表结构表名关键字段说明业务意义usersroletinyint0用户,1商家,2管理员,statustinyint0禁用,1启用角色与状态分离避免用字符串判别scenic_spotstype_id外键关联spot_types,route_id外键关联spot_routes,is_hottinyint景点与类型、路线强关联支持首页热榜筛选ordersuser_id,spot_id,order_status0待支付,1已支付,2已发货,3已完成,4已取消,pay_time状态机字段明确pay_time为datetime类型非timestamp避免时区自动转换collectionsuser_id,spot_id,collect_time用户收藏表无冗余字段靠联合索引(user_id, spot_id)防重复system_configconfig_key如site_name,config_value如智慧乡村旅游平台全局配置表避免硬编码执行后用 Navicat 或命令行验证SELECT COUNT(*) FROM users WHERE role 2; -- 应返回至少 1 条管理员记录账号 admin / 密码 123456 SELECT * FROM system_config WHERE config_key site_name;2.3 微信小程序端开发者工具配置与云开发兼容性处理项目未使用云开发所有接口均指向本地后端。打开微信开发者工具导入miniprogram/目录注意不是整个项目根目录关键修改在project.config.json和app.jsproject.config.json中appid字段需替换为你自己的测试号 AppID可在微信公众平台申请否则无法调用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 }发送若 token 过期fail回调中会wx.navigateTo({ url: /pages/login/login })。注意微信开发者工具默认开启「不校验合法域名」但真机调试时必须在「开发管理」中将http://localhost:8080加入「request 合法域名」实际生产需换为 HTTPS 域名。若真机报request:fail net::ERR_CONNECTION_REFUSED先确认后端 Tomcat 是否已启动且监听 8080 端口。2.4 一键脚本实操1-install.bat、2-run.bat、3-build.bat的真相项目提供了三个.bat脚本但它们并非全自动魔法1-install.bat本质是mvn clean install -Dmaven.test.skiptrue作用是编译 Java 代码、运行 MyBatis Generator若配置了、打包成tourism.war放入target/。执行前需确保pom.xml中plugin里的mybatis-generator-maven-plugin版本与generatorConfig.xml兼容本项目用 1.3.22-run.bat调用mvn tomcat7:run启动内嵌 Tomcat端口 8080适合开发调试。若报Address already in use: bind改pom.xml中port为 80813-build.bat执行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 用户无需关注。3. 角色权限落地三层角色如何通过 Filter Session SQL 实现数据隔离3.1 后台权限控制基于 Session 的角色路由拦截管理员、商家、普通用户的菜单入口完全不同系统未用 Shiro 或 Spring Security而是用原生FilterHttpSession实现轻量级控制。关键文件是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(); // 根据角色放行对应路径 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 { // 角色越权重定向到无权限页 req.setAttribute(msg, 无访问权限); req.getRequestDispatcher(/error/noAuth.jsp).forward(req, resp); } }逻辑说明Filter在每次请求时触发通过session.getAttribute(user)获取登录态再根据user.getRole()值判断 URI 前缀是否匹配。参数说明user.getRole()是tinyint类型值 0/1/2 严格对应数据库users.role字段不可用字符串admin比较。3.2 小程序端权限分流App.js 全局路由守卫小程序没有传统 Session权限控制依赖wx.setStorageSync(token)和后端返回的role字段。app.js中onLaunch方法完成首次鉴权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 字段 // 根据 role 跳转不同首页 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 }); // 管理员走独立登录页 } } } }); } } });参数说明userInfo.role由后端UserController.info()接口返回该接口从JwtUtil.parseToken(token)解析出用户 ID再查users表获取role字段。JWT 密钥在JwtUtil.java中硬编码为tourism_secret生产环境必须替换为随机密钥。3.3 数据库层隔离同一张表如何让不同角色看到不同数据最典型的场景是scenic_spots表管理员能看到全部景点商家只能看到自己发布的用户只能看到status1审核通过的。后端通过动态 SQL 实现!-- 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) !-- 商家可看自己待审核和已发布 -- /if if testrole 2 !-- 管理员 -- 11 !-- 查全部 -- /if /where ORDER BY create_time DESC /select逻辑说明MyBatis 的if标签根据传入的role参数动态拼接 WHERE 条件。参数说明#{userId}是商家 ID从SecurityUtil.getCurrentUserId()获取该工具类从ThreadLocal中取当前登录用户 ID避免 Controller 层重复传参。3.4 订单状态机从下单到完成的 5 个状态如何被三方协同更新订单流程是检验权限设计的试金石。orders表的order_status字段驱动整个生命周期状态值触发方触发条件后端逻辑0待支付用户提交订单后OrderService.createOrder()插入记录order_status01已支付用户支付成功回调PayNotifyController.notify()校验签名后update order_status1, pay_timenow()2已发货商家点击“发货”按钮MerchantOrderController.ship()校验role1且order_status1更新order_status2, ship_timenow()3已完成用户确认收货UserOrderController.confirm()校验role0且order_status2更新order_status3, finish_timenow()4已取消用户/商家/管理员任一角色调用取消接口OrderController.cancel()根据角色判断用户仅能取消order_status0商家可取消0/1管理员可取消全部关键约束OrderService.cancelOrder()方法中对商家取消操作加了双重校验if (order.getStatus() ! 0 order.getStatus() ! 1) { throw new ServiceException(商家只能取消待支付或已支付的订单); }4. 避坑指南那些让你调试到凌晨三点的典型问题与解法4.1 现象小程序登录后wx.request返回 401但后端日志显示 token 解析成功原因微信小程序wx.request默认不携带 Cookie而本项目后端JwtUtil从request.getHeader(Authorization)读取 token但小程序端header中未设置Authorization字段。解决检查utils/api.js中request方法确认header是否包含Authorization。常见错误是wx.getStorageSync(token)返回undefined因登录接口未正确存储应在login.js的success回调中显式写入wx.setStorageSync(token, res.data.data.token); wx.setStorageSync(userInfo, res.data.data.user);4.2 现象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 现象商家上传景点图片后页面显示空白数据库scenic_spots.image_path字段存的是upload/20230501/abc.jpg但浏览器访问http://localhost:8080/tourism/upload/20230501/abc.jpg报 404原因Tomcat 默认不提供upload/目录的静态资源服务需在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 现象管理员后台点击“删除景点”浏览器弹出确认框后页面无反应F12 查看 Network 发现DELETE请求返回 500原因ScenicSpotController.delete()方法中scenicSpotService.removeById(id)调用前未校验景点是否已被订单引用导致外键约束失败抛出SQLIntegrityConstraintViolationException。解决在ScenicSpotService.removeById()中增加前置检查// 先查是否有订单关联此景点 Long orderCount orderMapper.countBySpotId(id); if (orderCount 0) { throw new ServiceException(该景点已有订单无法删除); }4.5 现象MySQL 执行tourism.sql时scenic_spots表创建失败报错Specified key was too long; max key length is 767 bytes原因scenic_spots.name字段为varchar(200)使用utf8mb4字符集时单字符最多占 4 字节200×4800 767。解决修改tourism.sql中scenic_spots.name字段长度为varchar(191)191×4764 ≤ 767或在 MySQL 5.7 中执行SET GLOBAL innodb_file_formatBarracuda; SET GLOBAL innodb_file_per_tableON; SET GLOBAL innodb_large_prefixON; ALTER TABLE scenic_spots ROW_FORMATDYNAMIC;5. 真实业务缝合如何把“景点路线”模块从静态列表变成可交互的导航闭环5.1 路线数据结构设计为什么用spot_routes表而非 JSON 字段很多初学者会把路线点存成JSON字符串如{points:[{lat:30.1,lng:120.2,name:入口},{lat:30.2,lng:120.3,name:观景台}]}但这会导致无法按坐标范围查询、无法建立空间索引。本项目采用关系型设计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, -- 经度 sort_order INT NOT NULL DEFAULT 0, -- 排序序号 FOREIGN KEY (route_id) REFERENCES spot_routes(id) ON DELETE CASCADE );优势route_points表可对latitude/longitude建复合索引支持“查找距离某坐标 1km 内的所有路线点”sort_order字段保证前端渲染顺序ON DELETE CASCADE确保删除路线时自动清理点数据。5.2 小程序端路线展示Canvas 绘制折线 Marker 标注的实战代码pages/route/detail.js中onLoad获取路线数据后调用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); // 计算画布坐标将经纬度映射到画布像素 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轴翻转 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()计算所有点的经纬度极值用于归一化scaleX/scaleY将地理坐标转为画布像素Y 轴翻转是因为地图纬度北增南减而 Canvas Y 轴下增上减。参数说明dpr设备像素比确保高清屏显示清晰ctx.arc()绘制圆形 Marker起点绿色、终点橙色、中间蓝色符合用户认知。5.3 后端路线推荐基于用户历史订单的协同过滤简易实现当用户查看某景点时后端SpotController.recommendRoutes()接口会返回“常与该景点一同被订购的路线”。不用复杂算法用 SQL 关联即可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关联scenic_spots.name找出用户历史订单中景点名称匹配的路线点再按路线分组计数。虽简单但比纯随机推荐更贴近真实行为。5.4 从“能跑”到“能用”我每次交付前必做的三件事第一强制走一遍「用户下单→商家发货→用户确认→管理员查看统计」全流程用 Postman 模拟每个角色的请求头特别是Authorization验证orders.order_status字段是否按预期流转第二导出tourism数据库用mysqldump --skip-triggers --no-create-info tourism data_only.sql清除建表语句只留INSERT作为交付给导师的“纯净数据”第三在小程序project.config.json中把appid改为空字符串这样即使别人拿到代码也无法直接上线倒逼他们理解配置项含义。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表