news 2026/9/26 17:54:43

智慧乡村旅游小程序毕业设计:SSM+MySQL实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智慧乡村旅游小程序毕业设计:SSM+MySQL实现与避坑指南

简介:基于微信小程序与SSM框架的智慧乡村旅游服务平台毕业设计资源,面向计算机相关专业学生及需要快速搭建同类项目的开发者,提供了一套完整可运行的工程方案。资源包共895个文件,约40.48MB,涵盖Java后端代码、Vue前端组件、微信小程序页面、SQL数据库脚本、设计图标与界面截图,以及安装运行批处理脚本,可完整支撑从环境搭建到部署演示的全过程。系统设计了管理员、用户、商家三类角色,实现了旅游景点管理、路线规划、购物车、订单处理、收藏与充值等业务模块,具有较强的实用性。目前已有313人学习下载。资源同时包含毕业论文与视频演示,论文涉及可行性分析、系统设计及数据库设计等关键章节,便于理解开发思路和撰写毕业设计文档;视频演示则可直观查看功能运行效果,适合毕业设计参考与二次开发。

1. 智慧乡村旅游小程序毕业设计:这套SSM+MySQL方案到底帮你交付什么

毕业设计拿到一套小程序源码,最慌的不是看不懂代码,而是不知道从哪一步下手。智慧乡村旅游服务平台就是这样一套典型的微信小程序项目实例:小程序端面向游客,做景点浏览、民宿预订、路线推荐;后端用 SSM(Spring+SpringMVC+MyBatis)提供接口,MySQL 负责把用户、订单、景点、评论这些数据落库;交付物里还带着数据库脚本、毕业论文和视频演示。这套方案解决的核心问题,是用一套成熟的 Java 课程设计案例源码,把乡村旅游预订闭环跑通。它适合正在做毕业设计的学生,也适合想第一次完整接触小程序前后端联调的开发者。你要做的不是读懂每一行代码,而是先弄清楚它怎么启动、怎么联调、怎么改成自己的题目。

2. 数据库设计与 SSM 后端搭建:九张表和一个接口的最小实现

2.1 数据库设计:九张表怎么串起乡村旅游业务

拿到源码包后别急着启动项目,先打开数据库脚本。这套项目用的是 MySQL,脚本文件一般叫 rural.sql。用 Navicat 或 mysql workbench 导入后,我会先数一遍表:用户表、景点表、民宿表、路线表、订单表、评论表、收藏表、轮播图表、管理员表,一共九张。业务关系不复杂,用户对景点和民宿产生收藏、评论、订单三类行为,后台管理员维护内容。

用户表的设计有个细节,小程序的用户不是靠用户名密码,而是靠微信登录,所以用户表里要有一列 openid。管理员不走小程序,靠 username 和 password 登后台。两张核心表的建表语句如下:

-- 用户表:小程序用户与后台管理员共用 CREATE TABLE `t_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `openid` VARCHAR(64) DEFAULT NULL COMMENT '微信唯一标识,小程序登录用', `username` VARCHAR(50) DEFAULT NULL COMMENT '用户名,管理员登录用', `password` VARCHAR(64) DEFAULT NULL COMMENT '密码,存MD5值', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '小程序端展示昵称', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', `phone` VARCHAR(11) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 订单表:订单业务流转的核心 CREATE TABLE `t_order` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号,格式如R20240601120001', `user_id` INT NOT NULL COMMENT '下单用户ID', `house_id` INT DEFAULT NULL COMMENT '民宿ID,下单类型是民宿时有值', `scenic_id` INT DEFAULT NULL COMMENT '景点ID', `order_type` TINYINT DEFAULT 1 COMMENT '1民宿 2门票 3路线', `status` TINYINT DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已取消 3已完成', `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '实付金额', `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

这两张表有一个共同点:都用 InnoDB 引擎、utf8mb4 字符集。InnoDB 是为了订单表的事务,utf8mb4 是为了能存用户昵称里的 emoji。为什么订单表单独设计 order_no 而不是直接用自增 id?因为订单编号要展示给用户,自增 id 太容易暴露业务量,而且订单号要有可读性。我习惯用“业务字母+日期+流水号”的格式。

参数说明:openid 的 UNIQUE KEY 保证同一个微信号只能注册一次;amount 用 DECIMAL 不用 FLOAT,金额计算不能有浮点误差;status 给默认值 0,这样插入订单时不用显式传状态。如果你在建表时发现字段和我写的不一致,以你拿到的 rural.sql 为准,表结构没有标准答案,只要业务闭环完整就行。

2.2 Maven 工程与 SSM 装配:pom 依赖和两份 Spring 配置

这套项目是传统 SSM 结构,不是 SpringBoot。不少读者会问,现在都用 SpringBoot 了为什么还要 SSM?两个原因:一是不少课程大纲还沿 SSM 在讲,答辩时老师问的也是这套;二是 SSM 手写配置能把 Spring、SpringMVC、MyBatis 三者的协作关系讲清楚。如果你拿到的是 SpringBoot 改写版,原理一样,只是配置被自动装配替代了。

先看 pom.xml。Maven 会把依赖管理好,我一般会确认这几个关键依赖存在:

<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.15.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.15.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.10</version> </dependency>

版本号不是死的。Spring 5.2.x 和 MySQL 8.0.x 驱动是目前 SSM 项目里兼容性较好的组合;如果你本地 JDK 是 1.8,就别硬上 Spring 6,那需要 JDK 17。Druid 连接池的作用是管理数据库连接,避免每次请求都新建连接,initialSize 和 maxActive 这两个参数分别控制初始连接数和最大连接数,毕设项目给 5 和 20 足够。

然后是 SSM 里最容易配错的 spring-mybatis.xml。它负责三件事:数据源、SqlSessionFactory、Mapper 扫描:

<!-- spring-mybatis.xml 核心片段 --> <context:property-placeholder location="classpath:db.properties" /> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="mapperLocations" value="classpath:mapper/*.xml" /> <property name="typeAliasesPackage" value="com.rural.entity" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.rural.dao" /> </bean>

db.properties 里 jdbc.driver 要注意 MySQL 版本:5.x 用 com.mysql.jdbc.Driver,8.x 用 com.mysql.cj.jdbc.Driver。jdbc.url 里我习惯带上 useSSL=false、serverTimezone=Asia/Shanghai、characterEncoding=utf8 三个参数,后面两个对 MySQL 8 是必填,否则连库时报时区错误,中文也可能乱码。

上面的配置里,mapperLocations 指向 classpath:mapper/*.xml,意思是 MyBatis 的 SQL 语句写在 resources/mapper 目录下;typeAliasesPackage 简化实体类包名,这样在 XML 里写 resultType="User" 而不是全限定名。MapperScannerConfigurer 自动扫描 com.rural.dao 包,把接口和 XML 动态代理成 Bean。这一套如果配错,最常见的报错就是 mapper 绑定异常,后面避坑章会单独说。

2.3 从 DAO 到 Controller:一个景点列表接口的最小链路

SSM 项目的接口链路是固定的:Controller 接收参数→Service 处理业务→Mapper 查库→XML 写 SQL→JSON 返回。我建议先拿景点列表接口练手,因为它只读不写,最适合验证 SSM 是否装配成功。

// ScenicController.java @RestController @RequestMapping("/api/scenic") public class ScenicController { @Autowired private ScenicService scenicService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword) { PageHelper.startPage(page, size); List<Scenic> list = scenicService.queryList(keyword); PageInfo<Scenic> pageInfo = new PageInfo<>(list); return Result.ok(pageInfo); } }

几个参数要说明一下。page 是页码,size 是每页条数,keyword 是可选的关键词,前端首页传“热门”这个关键词就能拿到热门景点排序。PageHelper.startPage 是 MyBatis 的分页插件,它会在执行下一条查询 SQL 时自动拼接 limit,不需要手写分页。pom.xml 里需要额外引入 pagehelper 依赖。Result 是统一的返回结构,我一般设计成 {code:0, msg:"success", data:...} 三段式,code 非 0 表示业务错误,小程序端根据 code 判断是否弹提示。

Service 层和 Mapper 层不复杂,Service 里做空值判断,Mapper 接口定义 list 方法,对应的 XML 里写 SQL:

<!-- ScenicMapper.xml --> <mapper namespace="com.rural.dao.ScenicMapper"> <select id="list" resultType="Scenic" parameterType="map"> SELECT id, name, level, address, price, image, description FROM t_scenic <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY hot DESC, id DESC </select> </mapper>

这个 XML 里有三个常见写法:namespace 必须是 Mapper 接口的全限定名;id 必须和接口方法名一致;resultType 指向 Scenic 实体。这里用 where 标签而不是直接在 SQL 后面写 where 1=1,是为了让动态 SQL 更干净。如果你改动后启动报绑定异常,先查这三处是否对得上。

3. 小程序端对接后端:请求封装、登录态与页面渲染

3.1 微信小程序请求封装:BASE_URL、token 和缓存时间设置

原生小程序没有 axios,所有网络请求都走 wx.request。项目里一般会在 utils 目录下建一个 request.js 做统一封装,这样页面里不用重复写 url 前缀、请求头和错误处理。

// utils/request.js const BASE_URL = 'http://192.168.1.100:8080/rural'; // 联调地址,真机要写局域网IP function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); // GET请求做缓存,5分钟有效,减少重复请求 if (method === 'GET') { wx.setStorage({ key: url, data: { value: res.data.data, expire: Date.now() + 5 * 60 * 1000 } }); } } else if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(new Error('登录已过期,请重新登录')); } else { reject(new Error(res.data.msg || '请求失败')); } }, fail(err) { reject(err); } }); }); } module.exports = { request, BASE_URL };

这里的 BASE_URL 是后端 Tomcat 的访问地址,端口 8080,/rural 是项目上下文路径。本地联调时有个容易忽略的点:真机上不能写 localhost 或 127.0.0.1,那指向的是手机自己,要改成电脑在局域网里的 IP。缓存时间设置这个小功能不算必须,但加了之后用户反复进首页会明显感觉快,答辩时能说成性能优化点。

注意:真机预览时,BASE_URL 不能写 localhost,要写成电脑的局域网 IP,手机和电脑必须处于同一个 Wi-Fi。

封装之后,页面里调用就是一行 Promise:

const { request } = require('../../utils/request'); Page({ async loadScenics() { try { const data = await request('/scenic/list?page=1&size=10', 'GET'); this.setData({ scenics: data.list }); } catch (e) { wx.showToast({ title: e.message, icon: 'none' }); } } });

3.2 登录鉴权:wx.login 换 token,后端如何识别用户

小程序登录和网页不同,用户没有账号密码输入环节。标准的流程是:wx.login 拿到临时 code;把 code 发给后端;后端拿 code 去微信接口换 openid;后端生成 token 返给前端;前端把 token 存到 storage,以后每次请求都带上。

// pages/login/login.js const { request } = require('../../utils/request'); Page({ onLoad() { wx.login({ success: async (res) => { const code = res.code; try { const data = await request('/auth/login', 'POST', { code: code }); wx.setStorageSync('token', data.token); wx.setStorageSync('userId', data.userId); wx.switchTab({ url: '/pages/index/index' }); } catch (e) { wx.showToast({ title: '登录失败', icon: 'none' }); } } }); } });

wx.login 的 code 有效期很短,几分钟就会失效,所以拿到 code 要立即发给后端。后端 AuthController 的处理思路是:

// AuthController.java 登录接口核心逻辑 @PostMapping("/auth/login") public Result login(@RequestBody Map<String, String> body) { String code = body.get("code"); // 用code换openid,这里封装了微信接口调用 String openid = wxService.code2Session(code); User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(0, 8)); userService.add(user); } // 生成token并缓存,后续接口通过token识别用户 String token = UUID.randomUUID().toString().replace("-", ""); tokenService.cache(token, user.getId()); return Result.ok(new LoginVO(token, user.getId(), user.getNickname())); }

这个接口做了三件事:换 openid、查用户、不存在则注册。openid 是微信给每个用户在单个小程序下的唯一标识,同一个用户在你的小程序里 openid 永远不变,但换成另一个小程序 openid 就不同,所以不能拿 openid 当跨应用的全局 ID。token 我习惯用 UUID,简单够用;生产环境应该换 JWT 并控制过期时间,毕设用 UUID 存内存或 Redis 表都行。

3.3 首页渲染:轮播图、热门景点和导航栏适配

首页是用户打开小程序看到的第一屏,数据来源是轮播图和热门景点两个接口。页面结构上,swiper 组件负责轮播,普通 view 配合 wx:for 渲染景点卡片。

// pages/index/index.js const { request } = require('../../utils/request'); Page({ data: { banners: [], hotScenics: [], navBarHeight: 64 // 自定义导航栏高度,默认64px }, async onLoad() { this.initNavBar(); this.loadBanners(); this.loadHotScenics(); }, initNavBar() { // 动态获取顶部状态栏和胶囊按钮高度,适配不同机型 const menu = wx.getMenuButtonBoundingClientRect(); this.setData({ navBarHeight: menu.top + menu.height + 4 }); }, async loadBanners() { const banners = await request('/banner/list?type=home', 'GET'); this.setData({ banners: banners }); }, async loadHotScenics() { const data = await request('/scenic/list?sort=hot&limit=6', 'GET'); this.setData({ hotScenics: data.list }); } });

轮播图 banner 是后台管理端配置的,小程序端只负责展示。这里有一个常见问题:开发者工具里图片能看到,真机上不显示。原因是轮播图 URL 必须能通过域名校验,http 图片在小程序里会被拦。如果你用的是 http 图片,要么换成 https,要么在开发者工具里勾选不校验合法域名,但真机预览依然不行。

如果读者更熟悉 uniapp,可以把小程序端用 uniapp 重写,跨平台性更好,但毕业设计模板大多是原生小程序,改成 uniapp 要动页面所有标签,工作量不划算。这个选择留到论文的创新点部分作为展望写一句即可。导航栏高度这个话题,用 wx.getMenuButtonBoundingClientRect() 取胶囊按钮位置,是自定义导航栏的标准做法,不同机型差异明显,不能写死。

4. 订单与后台管理:把业务流程跑通的完整闭环

4.1 民宿预订与订单状态机

订单是这类平台的核心。订单表里 status 字段我定了四个值:0 待支付、1 已支付、2 已取消、3 已完成。状态只能单向流转,已支付不能回到待支付,已完成不能取消。这个规则要同时落到前端按钮和后台 Service 两个地方。

// OrderService.java 订单状态更新核心方法 public boolean updateStatus(Integer orderId, Integer fromStatus, Integer toStatus) { // 更新时带上原状态条件,防止并发下重复操作 int rows = orderMapper.updateStatus(orderId, fromStatus, toStatus); return rows == 1; }

这段代码的关键是 update 语句里带了 fromStatus 条件:

<!-- OrderMapper.xml --> <update id="updateStatus"> UPDATE t_order SET status = #{toStatus} WHERE id = #{orderId} AND status = #{fromStatus} </update>

为什么必须带 fromStatus?因为用户在待支付页面点了取消,同时后台管理员也在操作这条订单,如果两边都不带状态条件,后执行的会把先执行的结果覆盖掉。带条件之后,只有一条 update 语法生效,返回 1,另一条返回 0,从语法层面杜绝了并发脏数据。这个方法虽然只有几行,却是我在答辩时最喜欢讲的点,因为它能体现对并发控制的理解。

4.2 后台管理端:SSM 项目里的 JSP 管理页面

很多毕业设计的小程序后台不是另一个小程序,而是 SSM 框架自带的 JSP 页面,跑在 Tomcat 的 /admin 路径下。管理员登录后可以管理景点、民宿、订单、轮播图。JSP 页面用 JSTL 标签循环渲染数据,配合 PageInfo 做分页按钮。

<!-- admin/scenic_list.jsp 景点管理列表核心片段 --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <table class="table table-bordered"> <thead> <tr> <th>ID</th><th>景点名称</th><th>等级</th><th>价格</th><th>状态</th><th>操作</th> </tr> </thead> <tbody> <c:forEach items="${page.list}" var="s"> <tr> <td>${s.id}</td> <td>${s.name}</td> <td>${s.level}</td> <td>${s.price}</td> <td>${s.status == 1 ? '上架' : '下架'}</td> <td> <a href="${ctx}/admin/scenic/edit?id=${s.id}">编辑</a> <a href="${ctx}/admin/scenic/delete?id=${s.id}" onclick="return confirm('确认删除?')">删除</a> </td> </tr> </c:forEach> </tbody> </table>

JSP 这边的坑集中在两点。第一,admin 路径下所有页面都要做登录校验,常见做法是一个 HandlerInterceptor 拦截器,判断 session 里有没有 admin 用户,没有就重定向到登录页。第二,JSP 里的 EL 表达式 ${page.list} 依赖后端 ModelAndView 里 put 的值,如果你改了 Controller 方法的返回值类型,这里也要同步改。上架状态在新增编辑页面一般用单选框 radio 绑定 status 字段,注意回显时 checked 的判断逻辑。一个经常被忽略的细节是后台管理端的静态资源,CSS 和 JS 如果加载不出来,通常是 web.xml 里 DispatcherServlet 的映射把静态文件拦截了,这个在避坑章会展开讲。

4.3 模拟支付:没有商户号时怎么跑通订单闭环

毕业设计拿不到微信支付商户号,这是常态。项目里常见做法是写一个 mockPay 接口,前端在支付确认页直接调这个接口,把订单状态从待支付置为已支付,并记录支付时间。论文里要如实写这是模拟支付,不要写真实接入了微信支付,答辩时老师问支付逻辑要能说清楚。

// OrderController.java 模拟支付接口 @PostMapping("/order/mockPay") public Result mockPay(@RequestParam Integer orderId, @RequestParam Integer userId) { Order order = orderMapper.findById(orderId); // 1. 订单必须存在且属于当前用户 if (order == null || !order.getUserId().equals(userId)) { throw new BizException("订单不存在或无权操作"); } // 2. 只有待支付状态才能支付 if (order.getStatus() != 0) { throw new BizException("当前订单状态不可支付"); } // 3. 更新为已支付并写支付时间 orderMapper.updateStatus(orderId, 0, 1); orderMapper.updatePayTime(orderId, new Date()); return Result.ok("模拟支付成功"); }

模拟支付接口的价值在于把整条业务链跑通:用户下单选民宿,生成待支付订单,调 mockPay 变成已支付,然后可以继续做评价、确认入住、完成订单。这里三个校验顺序不要乱:先校验订单归属,再校验状态,最后执行更新。如果把状态校验放最后,用户传一个不属于自己的订单号也能触发状态变更,这是真实项目里必须避免的逻辑漏洞。

5. 本地联调避坑手册:SSM+微信小程序最常见的几个坑

5.1 真机请求不到接口:合法域名和设备地址

现象:开发者工具里接口正常返回,点击预览后用手机打开,页面空白,控制台报 wx.request:fail。

原因有三个方向。第一,小程序真机请求必须走校验通过的域名,http 接口在真机上会被拦;第二,BASE_URL 写的是 localhost,手机访问的是自己,不是电脑;第三,电脑防火墙把 8080 端口拦了。

解决:开发阶段在微信开发者工具的“详情→本地设置”里勾选“不校验合法域名”,这只是临时方案;BASE_URL 改成电脑局域网 IP,比如 192.168.x.x,手机和电脑要连同一个 Wi-Fi;防火墙入站规则放行 8080 端口。如果条件允许,最稳妥是部署到有域名的服务器上,但毕业设计本地演示往往没这个条件,所以局域网 IP 是主要方案。

5.2 MySQL 连接失败:驱动、时区与 socket 路径

现象:Tomcat 启动时报 Cannot create PoolableConnectionFactory,或 Access denied。本地命令行连接时报 error 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。

原因:MySQL 服务没启动,连接的是 socket 而不是 TCP 端口;驱动类和 MySQL 版本不匹配;密码错误;MySQL 8 时区参数缺失。

解决:先确认 MySQL 装好并能启动。用 mysql -uroot -p 试一下,报 2002 错误就说明服务没起来,Linux 下 service mysqld start,Windows 下到服务管理器启动。驱动方面,MySQL 5.x 用 com.mysql.jdbc.Driver,8.x 用 com.mysql.cj.jdbc.Driver,别混。url 里的 serverTimezone=Asia/Shanghai 和 useSSL=false 对 MySQL 8 是必填项。选版本时如果自己装的是 MySQL 8.0,就用 mysql-connector-java 8.0.x,按照 mysql 官方安装教程配置即可。

5.3 mapper 绑定异常:namespace、id 和路径

现象:项目启动后调用接口,后台报 Invalid bound statement (not found): com.rural.dao.ScenicMapper.list。

原因:三处不一致。Mapper 接口的全限定名和 XML 里的 namespace 不一致;接口方法名和 XML 里的 id 不一致;spring-mybatis.xml 里 mapperLocations 没扫到 XML 文件。

解决:按顺序排查。先看 ScenicMapper.java 第一行 package 是不是 com.rural.dao,再看 ScenicMapper.xml 里 namespace 是不是 com.rural.dao.ScenicMapper。确认 id 和方法名一致。前两项都对但还报错,多数是 Maven 没把 src/main/java 下的 XML 打进 target 目录,pom.xml 里在 build 节点加 resources,把 *.xml 和 *.properties 都包含进去。这个坑在 SSM 项目里出现频率最高,也是新手最容易卡住的地方。

5.4 中文乱码:连接、表结构和 Tomcat 三层

现象:小程序端景点名称显示一堆问号,后台管理端中文正常但存进数据库变成乱码,或者反过来。

原因:三层都有可能。第一层是连接 url 缺少 characterEncoding=utf8,中文在传输过程中被转成 ISO-8859-1;第二层是表和库本身是 latin1 字符集,存不了中文;第三层是 Tomcat 接收 POST 参数时 URIEncoding 不对。

解决:连接 url 在 db.properties 里改成 jdbc:mysql://localhost:3306/rural?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。数据库表统一用 utf8mb4,和 2.1 节建表 SQL 保持一致。Tomcat 的 server.xml 里 Connector 标签加 URIEncoding="UTF-8"。改完这三处重启,不要只改一处,否则查来查去都是白费时间。

5.5 后台 CSS 样式全丢:DispatcherServlet 拦截静态资源

现象:登录后台管理端,页面能打开但 CSS 和 JS 全加载不出来,浏览器 F12 看到一堆 404。

原因:web.xml 把 DispatcherServlet 映射到 / 或者 /*,所有请求都交给 SpringMVC 处理,/admin/css/style.css 这种静态资源也被当成 Controller 路径去找了,找不到就 404。

解决:三个方案选一个。web.xml 里把 DispatcherServlet 映射改成 *.do;或者在 spring-mvc.xml 里加 mvc:default-servlet-handler 让 Tomcat 默认 Servlet 处理静态资源;或者用 mvc:resources 单独放行。毕设项目里改动最小的是方案二,加一行配置就能解决。

这五个坑是我在做这类项目时按出现频率排出来的。其中 mapper 绑定异常和中文乱码是 SSM 老项目的高发区,小程序真机请求是微信开发者特有的坑。遇到问题时别急着改代码,先按现象定位到层,连接层、配置层、编译层分开排查,比盲目改要快得多。

6. 论文、视频与验收:把源码变成能过答辩的交付物

源码跑通只是第一步,毕业设计真正要交的是论文、视频和可演示的系统。论文部分,ER 图可以直接从数据库脚本里逆向生成,用 mysql workbench 的逆向工程会比手画快;流程图要覆盖用户下单、后台审核两条主线,不要画一幅包罗万象的大图,分小节画更容易讲清楚。核心代码不要整段截图,选订单状态更新这一段,配上文字说明为什么用带条件的 update。

视频演示我一般按两条线拍:用户线先演示微信授权登录、浏览首页轮播、查看景点详情、预订民宿、模拟支付、查看订单状态;管理线演示管理员登录、新增景点、上下架民宿、处理订单。每条线控制在 3 分钟以内,用手机录屏加电脑端后台录屏拼接。拍摄前把演示数据准备好,数据库里的管理员账号、测试用户、几条已支付订单都要存在,不然现场造数据会手忙脚乱。

验收环节容易被忽略的是数据库初始化。拿到 rural.sql 后,把 admin 账号密码、测试用户的 openid、演示用的景点图片都核对一遍。我当初做这个项目最后悔的就是没提前检查数据库里有没有演示数据,答辩前一天发现景点图片全是外链,换成本地图片花了一晚上。所以建议你在写论文之前,先把数据库脚本重新导入一遍,在干净环境里走通用户下单全流程,确认管理员后台能登录。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 17:54:26

iOS中NSData安全使用与内存泄漏避坑指南

简介&#xff1a;本资源是一份面向iOS初学者与进阶开发者的Objective-C基础实践代码包&#xff0c;聚焦Foundation框架核心类NSData的数据处理能力。压缩包共6个文件&#xff0c;包含Xcode工程配置文件&#xff08;pbxproj、pbxuser、mode1v3&#xff09;、项目信息配置&#x…

作者头像 李华
网站建设 2026/9/26 17:51:57

agent-native实践指南:如何把智能体真正用起来

最近“agent-native”这个词在圈子里讨论度特别高&#xff0c;产品群里、架构评审会上、技术博客里到处都在聊。很多团队嘴上说着要搞智能体原生应用&#xff0c;但实际上还是老一套&#xff1a;做个聊天窗口、接个模型API、把原来的业务流程套个对话框外壳&#xff0c;就说是a…

作者头像 李华
网站建设 2026/9/26 17:51:25

Python Flask校园失物招领系统:关键词匹配算法实战

校园里丢东西这事&#xff0c;几乎每天都在发生。图书馆落下一张校园卡&#xff0c;操场看台丢一副耳机&#xff0c;食堂吃完饭后伞还在门口挂着&#xff0c;人已经回宿舍了——而另一边&#xff0c;保洁阿姨捡到一堆东西拍在群里&#xff0c;问有没有人认识失主。消息刷得太快…

作者头像 李华
网站建设 2026/9/26 17:51:04

Agentic AI提示系统分布式锁设计:从事故到落地实践

我最早意识到Agentic AI提示系统需要认真对待分布式锁&#xff0c;是因为一次让我至今印象深刻的线上事故。当时提示系统刚做完水平扩展&#xff0c;正准备灰度一批新的Agent提示词版本&#xff0c;结果发布完成不到十分钟&#xff0c;线上反馈Agent行为出现回退&#xff1a;明…

作者头像 李华
网站建设 2026/9/26 17:49:55

LLM服务研究的数据集中心:统一负载格式与实验可复现性

LLM服务研究做了大半年&#xff0c;我越来越觉得最拖后腿的不是推理引擎本身&#xff0c;而是找不到一份"能统一认知"的实验数据。前阵子我把两个公开的请求日志丢进同一套调度器里对比测试&#xff0c;发现同一个算法的尾时延差异&#xff0c;竟然比算法带来的优化幅…

作者头像 李华