简介:这份资源是面向计算机专业学生与Java初学者的一套完整毕业设计/期末大作业方案,主题为乌鲁木齐景区导览系统,采用微信小程序前端搭配SSM框架与MySQL数据库实现。包内包含论文、PPT、需求分析文档及演示视频,覆盖从选题立项到答辩展示的完整链路,适合需要快速搭建景区导览类项目或参考小程序开发流程的读者。压缩包为zip格式,整体约47.96MB,主要文件类型包括论文与需求分析文档、答辩PPT、演示视频及项目源码,分别用于理论阐述、成果汇报、功能演示与代码复现。系统功能涵盖景区文字图片浏览、景点语音讲解、热门景区标签推荐与筛选、景区内地图导览,以及后台对景区信息统计和点击浏览记录的集中管理,模块划分清晰,便于按需拆解学习。目前已有43人学习下载,可帮助读者理解小程序与SSM整合思路、数据库表结构设计及导览类功能实现方式,为课程设计与毕业设计提供可参考的完整范例。
1. 乌鲁木齐景区导览系统:从一份毕设压缩包到能跑起来的小程序
很多同学拿到「基于微信小程序+SSM+Mysql的乌鲁木齐景区导览系统」这类题目时,第一反应是去搜现成源码,结果下载下来一堆压缩包,解压之后发现后端跑不起来、数据库连不上、小程序端白屏,最后连答辩都撑不过去。这个标题背后其实是一套非常典型的 Java Web 毕设技术栈:微信小程序做前端展示,SSM(Spring + SpringMVC + MyBatis)做后端服务,Mysql 做数据存储,业务场景是景区导览——景点列表、详情、地图定位、语音讲解、路线推荐这些。它适合正在做毕设的计算机专业学生,也适合想练手小程序全栈开发的新手。真正难的不是写代码,而是把三端串起来:小程序怎么调后端接口、SSM 怎么配、Mysql 表怎么设计、数据怎么灌进去。这篇就按我实际搭过一遍的路径,把每个环节拆开讲清楚,让你拿到压缩包之后知道先动哪里、后动哪里,哪里最容易翻车。
2. 环境准备:JDK、Tomcat、Mysql 和微信开发者工具怎么装才不打架
2.1 后端三件套的版本选择与安装顺序
SSM 项目对版本比较敏感,尤其是 Spring 和 MyBatis 的兼容性。我一般建议用 JDK 8(不是 11 也不是 17),因为大部分毕设源码的 pom.xml 里写的都是 1.8,换高版本容易遇到Unsupported class file major version这种玄学报错。Tomcat 用 8.5 或 9.0 都行,但注意 Tomcat 10 把 javax 包名改成了 jakarta,老项目直接跑不起来。Mysql 用 5.7 或 8.0,8.0 的驱动类名是com.mysql.cj.jdbc.Driver,5.7 是com.mysql.jdbc.Driver,这个在配置数据源的时候必须对上。
安装顺序建议:先装 JDK,配好JAVA_HOME和PATH;再装 Mysql,记住 root 密码;然后解压 Tomcat,改一下conf/server.xml里的端口,避免和已占用端口冲突;最后装微信开发者工具,用微信扫码登录。验证 JDK 装好没有,命令行敲:
java -version javac -version两条命令都能输出版本号才算成功。Mysql 装完之后验证:
mysql -u root -p输入密码能进mysql>提示符就对了。如果报error 2002 (hy000): can't connect to local mysql server through socket '/tmp/mysql.sock',说明 Mysql 服务没启动,Linux 下用systemctl start mysqld,Windows 下在服务面板里启动 MySQL 服务。
提示:Mysql 8.0 默认认证插件是
caching_sha2_password,老版本的 JDBC 驱动连不上,要么升级驱动,要么执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';改回旧认证方式。
2.2 微信开发者工具的项目导入与基础配置
微信开发者工具下载稳定版就行,不用追最新版。导入项目时,目录选到小程序端的根目录,AppID 可以选「测试号」,这样不用注册也能跑。导入之后如果编译报错,先看project.config.json里的miniprogramRoot指向对不对,很多压缩包里这个路径写的是作者本机的绝对路径,你得改成相对路径。
小程序端调后端接口有个硬性限制:开发阶段必须在开发者工具里勾选「不校验合法域名」。因为你的 SSM 后端跑在localhost:8080,没有备案域名也没有 HTTPS,不勾这个选项请求全部会被拦截。勾选位置在「详情」→「本地设置」→「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。
另外,小程序请求封装建议统一走一个request.js,把 baseUrl 抽出来,后面换服务器地址只改一个地方:
// utils/request.js const BASE_URL = 'http://localhost:8080/scenic'; function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json' }, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else { reject(res); } }, fail: reject }); }); } module.exports = { request };这段代码把wx.request包了一层 Promise,调用的时候可以用async/await,比回调嵌套清爽得多。BASE_URL里的scenic是后端项目的 context path,要和 Tomcat 部署时的路径一致,否则会 404。
3. SSM 后端搭建:从 web.xml 到 MyBatis 映射文件的完整链路
3.1 用 Maven 组织依赖与 Spring 配置文件拆分
SSM 项目的依赖管理靠 Maven,核心依赖就几块:Spring 的spring-context、spring-webmvc、spring-jdbc、spring-tx,MyBatis 的mybatis和mybatis-spring,数据库驱动mysql-connector-java,连接池用 Druid 或 C3P0,日志用 Log4j 或 SLF4J。pom.xml 里这些依赖的版本要互相对得上,我一般用 Spring 5.2.x + MyBatis 3.5.x + Druid 1.2.x 这个组合,比较稳。
配置文件通常拆成三个:applicationContext.xml管数据源、SqlSessionFactory、事务;spring-mvc.xml管 Controller 扫描、视图解析器、静态资源;web.xml管 DispatcherServlet 和 ContextLoaderListener 的加载。这种拆法是为了让 Spring 容器和 SpringMVC 容器各管各的,避免事务不生效这种经典坑。
数据源配置示例:
<!-- applicationContext.xml 片段 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/scenic_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean>serverTimezone=Asia/Shanghai这个参数在 Mysql 8.0 下必须加,不加会报时区错误。useSSL=false是关掉 SSL 连接,本地开发没必要开,开了反而容易连不上。initialSize和maxActive是连接池的初始连接数和最大连接数,毕设项目 5 和 20 足够了。
3.2 景区数据表设计与 MyBatis 映射文件编写
景区导览系统的核心表大概四张:scenic_spot(景点表)、scenic_category(分类表)、user(用户表)、comment(评论表)。景点表的关键字段包括id、name、description、latitude、longitude、image_url、category_id、create_time。经纬度用decimal(10,6)存,精度够用。
建表 SQL:
CREATE TABLE `scenic_spot` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '景点名称', `description` text COMMENT '景点描述', `latitude` decimal(10,6) DEFAULT NULL COMMENT '纬度', `longitude` decimal(10,6) DEFAULT NULL COMMENT '经度', `image_url` varchar(255) DEFAULT NULL COMMENT '封面图', `category_id` int(11) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意utf8mb4而不是utf8,因为景点描述里可能有 emoji 或者特殊字符,utf8存不下会报错。create_time设了默认值CURRENT_TIMESTAMP,插入的时候不用手动传。
对应的 MyBatis Mapper 接口和 XML:
// ScenicSpotMapper.java public interface ScenicSpotMapper { List<ScenicSpot> selectAll(); ScenicSpot selectById(Integer id); List<ScenicSpot> selectByCategory(Integer categoryId); }<!-- ScenicSpotMapper.xml --> <mapper namespace="com.scenic.mapper.ScenicSpotMapper"> <select id="selectAll" resultType="com.scenic.entity.ScenicSpot"> SELECT * FROM scenic_spot ORDER BY create_time DESC </select> <select id="selectById" parameterType="int" resultType="com.scenic.entity.ScenicSpot"> SELECT * FROM scenic_spot WHERE id = #{id} </select> <select id="selectByCategory" parameterType="int" resultType="com.scenic.entity.ScenicSpot"> SELECT * FROM scenic_spot WHERE category_id = #{categoryId} </select> </mapper>namespace必须和 Mapper 接口的全限定名一致,否则 Spring 启动时会报Invalid bound statement。resultType写实体类的全限定名,字段名和数据库列名不一致时要么起别名要么配resultMap。我一般建议字段名用驼峰、列名用下划线,然后在 MyBatis 配置里开mapUnderscoreToCamelCase=true,省得每个都写 resultMap。
3.3 Controller 层接口定义与跨域处理
Controller 负责接收小程序端的请求,返回 JSON。景区列表接口大概长这样:
@RestController @RequestMapping("/api/spot") public class ScenicSpotController { @Autowired private ScenicSpotService scenicSpotService; @GetMapping("/list") public Map<String, Object> list() { Map<String, Object> result = new HashMap<>(); List<ScenicSpot> spots = scenicSpotService.getAll(); result.put("code", 200); result.put("data", spots); return result; } @GetMapping("/detail/{id}") public Map<String, Object> detail(@PathVariable Integer id) { Map<String, Object> result = new HashMap<>(); ScenicSpot spot = scenicSpotService.getById(id); result.put("code", 200); result.put("data", spot); return result; } }@RestController等于@Controller+@ResponseBody,返回的对象自动转 JSON。跨域问题在小程序开发阶段其实不存在,因为小程序不是浏览器,没有同源策略。但如果你用浏览器调试后端接口,就需要加 CORS 配置,在spring-mvc.xml里加:
<mvc:cors> <mvc:mapping path="/api/**" allowed-origins="*" allowed-methods="GET,POST"/> </mvc:cors>注意:
allowed-origins="*"只在开发阶段用,上线要改成具体域名。
4. 小程序端页面开发:景点列表、详情页与地图定位的落地写法
4.1 景点列表页的数据请求与渲染
列表页的核心逻辑是onLoad时调接口拿数据,setData渲染到页面。WXML 里用wx:for循环:
// pages/spot/list.js const { request } = require('../../utils/request'); Page({ data: { spots: [], loading: true }, onLoad() { this.loadSpots(); }, async loadSpots() { try { const res = await request({ url: '/api/spot/list' }); if (res.code === 200) { this.setData({ spots: res.data, loading: false }); } } catch (e) { wx.showToast({ title: '加载失败', icon: 'none' }); this.setData({ loading: false }); } }, goDetail(e) { const id = e.currentTarget.dataset.id; wx.navigateTo({ url: `/pages/spot/detail?id=${id}` }); } });e.currentTarget.dataset.id对应 WXML 里><view wx:for="{{spots}}" wx:key="id" bindtap="goDetail"><map latitude="{{spot.latitude}}" longitude="{{spot.longitude}}" markers="{{markers}}" scale="15" style="width:100%;height:300rpx;"/>
markers需要在 JS 里构造:
this.setData({ markers: [{ id: 1, latitude: spot.latitude, longitude: spot.longitude, title: spot.name, width: 30, height: 30 }] });语音讲解如果只是播放固定音频,用wx.createInnerAudioContext():
const audioCtx = wx.createInnerAudioContext(); audioCtx.src = spot.audioUrl; audioCtx.play();注意InnerAudioContext在页面onUnload时要destroy(),不然会内存泄漏。如果要做 TTS 动态合成,那需要接第三方服务,毕设里一般用预录音频就够了。
4.3 用户登录与缓存策略
小程序登录走wx.login拿 code,传给后端换 openid。后端用code2Session接口换,但毕设里为了简化,很多直接在小程序端生成一个本地用户标识存wx.setStorageSync。我建议还是走后端,流程是:
wx.login({ success: async (res) => { const result = await request({ url: '/api/user/login', method: 'POST', data: { code: res.code } }); wx.setStorageSync('token', result.data.token); } });缓存时间设置用wx.setStorageSync配合过期时间戳:
const CACHE_TIME = 30 * 60 * 1000; // 30分钟 function setCache(key, value) { wx.setStorageSync(key, { value, expire: Date.now() + CACHE_TIME }); } function getCache(key) { const cache = wx.getStorageSync(key); if (cache && cache.expire > Date.now()) { return cache.value; } wx.removeStorageSync(key); return null; }这样景点列表可以缓存 30 分钟,减少请求次数。
5. 避坑与排查:数据库连不上、接口 404、地图不显示的 5 个血泪记录
5.1 现象:启动 Tomcat 报Cannot load JDBC driver class
原因:pom.xml 里 mysql 驱动依赖的 scope 写成了provided,或者根本没引入。解决:确认依赖是compile级别,并且mysql-connector-java的版本和 Mysql 服务端版本匹配。8.0 的服务端用 8.0.x 的驱动,5.7 用 5.1.x。
5.2 现象:小程序请求后端返回 404
原因:BASE_URL里的 context path 和 Tomcat 部署路径不一致。比如后端 war 包部署后访问路径是/scenic,但代码里写的是/scenicSystem。解决:在 Tomcat 的webapps目录下看 war 包解压后的文件夹名,那个就是 context path。或者在 IDEA 的 Run Configuration 里看 Deployment 的 Application context。
5.3 现象:地图组件显示空白
原因:latitude和longitude传的是字符串而不是数字,或者经纬度写反了。解决:在 JS 里用parseFloat()转一下,并且确认纬度在前、经度在后。乌鲁木齐的纬度大概 43.8,经度 87.6,如果发现地图跑到南极去了,就是写反了。
5.4 现象:MyBatis 查询返回字段为 null
原因:数据库列名是下划线风格,实体类是驼峰,没开自动映射。解决:在mybatis-config.xml里加<setting name="mapUnderscoreToCamelCase" value="true"/>,或者手动写 resultMap。
5.5 现象:小程序真机调试请求失败,开发者工具正常
原因:真机不认localhost,必须用局域网 IP。解决:把BASE_URL改成电脑的局域网 IP,比如http://192.168.1.100:8080/scenic,并且确保手机和电脑在同一个 WiFi 下。同时开发者工具里「不校验合法域名」在真机上不生效,需要在微信公众平台配置服务器域名,或者用真机调试模式绕过。
6. 进阶技巧:用存储过程和分页插件把景区查询性能提上去
景区数据量大了之后,列表查询会变慢。两个优化方向:一是用 Mysql 存储过程做复杂查询,二是用 PageHelper 做分页。
存储过程适合做统计类查询,比如「查询每个分类下的景点数量」:
DELIMITER // CREATE PROCEDURE count_by_category() BEGIN SELECT c.name, COUNT(s.id) AS spot_count FROM scenic_category c LEFT JOIN scenic_spot s ON c.id = s.category_id GROUP BY c.id; END // DELIMITER ;调用的时候CALL count_by_category();就行。存储过程的好处是逻辑在数据库层,减少网络传输,但缺点是调试麻烦、移植性差,毕设里用一两个展示一下就行,别全用。
分页用 PageHelper 更实用。在 pom.xml 加依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency>在 MyBatis 配置文件里加插件:
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> </plugin> </plugins>Controller 里调用:
@GetMapping("/page") public Map<String, Object> page(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize) { PageHelper.startPage(pageNum, pageSize); List<ScenicSpot> list = scenicSpotService.getAll(); PageInfo<ScenicSpot> pageInfo = new PageInfo<>(list); Map<String, Object> result = new HashMap<>(); result.put("code", 200); result.put("data", pageInfo.getList()); result.put("total", pageInfo.getTotal()); return result; }PageHelper.startPage必须紧跟在查询方法之前,中间不能插入其他数据库操作,否则分页会乱。PageInfo封装了总记录数、总页数、当前页等,小程序端拿total做上拉加载判断。
验证分页有没有生效,看控制台打印的 SQL 有没有LIMIT关键字。如果没有,检查插件配置是不是被 Spring 的配置文件覆盖了。我一般会在applicationContext.xml里显式配SqlSessionFactoryBean的plugins属性,而不是只写在 mybatis-config 里,避免加载顺序问题。
最后说个习惯:每次改完配置,先重启 Tomcat 再测,别热部署,SSM 的热部署经常出玄学问题。数据库改完表结构,记得同步更新实体类和 Mapper XML,不然查出来的字段对不上。这套东西搭一遍大概两三天,跑通之后后面加功能就是复制粘贴改改,希望帮到你。
本文还有配套的精品资源,点击获取