SSM+微信小程序小区管理系统毕设实战:从建表到部署答辩

发布时间:2026/10/5 5:11:24
SSM+微信小程序小区管理系统毕设实战:从建表到部署答辩
简介这是一套面向高校计算机专业毕业设计的完整项目资料主题为基于微信小程序的小区管理系统采用SSM框架搭建后台、Vue构建后台页面、MySQL存储数据开发环境兼容JDK1.8及Eclipse、MyEclipse、STS、IDEA等主流工具适合正在准备毕设或需要同类项目参考的开发者。系统区分管理员与用户两种角色管理员可管理个人中心、用户、投诉建议、基础数据、房屋信息、故障维修、公告、入住登记及轮播图用户可注册登录、查看房屋与公告、提交故障维修、投诉建议和入住登记。资源包共786个文件涵盖98个Java源码、129个Vue组件、123张PNG图片、75个JS脚本以及小程序端的WXSS、WXML等文件另含SQL脚本、论文、答辩PPT、开题报告、环境工具包与安装教程压缩包约33.82MB。目前已有85人学习可帮助读者快速获取完整赛题方案、数据库结构与部署排错思路。1. 从一份 SSM 微信小程序的小区管理系统说起它到底能跑通什么很多同学做毕设时都会卡在同一个地方想做一个「小区管理系统」脑子里只有增删改查但真到动手前端用网页还是小程序、后端用 Servlet 还是 SSM、数据库几张表、权限怎么分全是一团乱麻。这个标题指向的方案本质是一套前后端分离的落地组合后端用 SSMSpring SpringMVC MyBatis提供接口前端用微信小程序做业主端管理端可以配一个简单的后台页面数据库用 MySQL 存小区、楼栋、住户、报修、缴费这些核心数据。它解决的不是高并发难题而是「怎么在有限时间里把一套结构完整、能演示、能答辩的系统跑起来」。适合的人群很明确正在做 Java 方向毕设的本科生、需要一套可复用后台模板的初级开发者以及想借这个项目把 SSM 注解、小程序请求封装、分页查询这些高频面试点串一遍的人。下面我按实际落地的顺序把选型、建表、接口、小程序对接和踩坑点一层层拆开。2. 为什么这套系统选 SSM 而不是 Spring Boot以及环境怎么搭2.1 SSM 在毕设场景里的真实优势先说实话如果单纯从开发效率看Spring Boot 加 MyBatis-Plus 确实更快。但毕设场景有个绕不开的现实很多学校的指导老师明确要求体现 Spring、SpringMVC、MyBatis 的整合过程答辩时也会问「你的 IoC 容器怎么配的」「事务在哪一层控制的」。SSM 虽然配置繁琐但每一层都是显式的XML 和注解混用的方式反而方便你在论文里写清楚架构。另一个原因是SSM 的依赖体积小一台 8G 内存的笔记本同时开 IDEA、MySQL、微信开发者工具不会像跑微服务那样风扇狂转。从技术点上说SSM 整合的核心是三件事Spring 负责 Bean 管理和声明式事务SpringMVC 负责 URL 映射和参数绑定MyBatis 负责 SQL 映射和结果集封装。小区管理系统里的业务天然适合这种分层——住户信息、楼栋信息、报修工单、缴费记录每张表对应一个 MapperService 层做业务组合Controller 层只做参数校验和返回。常见做法是把applicationContext.xml拆成spring-dao.xml、spring-service.xml、spring-mvc.xml三个文件分别扫描不同包避免事务配置和 MVC 配置互相污染。2.2 环境搭建与依赖版本选择我一般会锁定一套经过验证的版本组合避免因为版本漂移导致启动报错。下面这套配置在 JDK 8 和 Tomcat 8.5 下跑得很稳组件版本说明JDK1.8SSM 生态对高版本 JDK 兼容性一般Spring5.3.x最后一个支持 JDK 8 的稳定大版本MyBatis3.5.x与 spring-mybatis 1.3.x 配合MySQL5.7 或 8.08.0 需要改驱动类和时区Druid1.2.x连接池方便监控 SQLTomcat8.5 或 9.0不要用 Tomcat 10包名变了Maven 的pom.xml里重点加这几块依赖注意spring-webmvc、spring-jdbc、mybatis-spring三个必须同时存在缺一个都会在启动时报NoClassDefFoundError!-- Spring 核心与 MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.30/version /dependency !-- MyBatis 与整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.1/version /dependency !-- 数据库驱动与连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency参数说明mybatis-spring的 2.x 版本对应 MyBatis 3.5如果你用 1.3.x 配 MyBatis 3.4 也可以但不要混用。MySQL 8.0 的驱动类要写成com.mysql.cj.jdbc.DriverURL 后面加?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8否则中文乱码和时区报错会轮流出现。Druid 的initialSize设 5、maxActive设 20 就够毕设用了设太大反而拖慢启动。2.3 web.xml 与 spring-mvc.xml 的关键配置web.xml里要做两件事注册 Spring 的ContextLoaderListener加载父容器注册DispatcherServlet加载 MVC 子容器。父容器扫描 Service 和 Dao子容器只扫描 Controller这样事务不会作用到 Controller 上。常见翻车点是两个容器都扫描了Service导致 Bean 被创建两次事务失效。!-- web.xml 片段 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-dao.xml,classpath:spring-service.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping逻辑说明ContextLoaderListener先启动创建父容器DispatcherServlet随后启动创建子容器并持有父容器的引用。子容器能拿到父容器的 Bean反过来不行。所以 Controller 里可以注入 Service但 Service 里不要注入 Controller。url-pattern用/而不是/*否则静态资源会被拦截小程序的图片和后台的 JS 都加载不出来。3. 数据库表设计与 SSM 三层代码落地3.1 小区管理系统的六张核心表不要一上来就设计十几张表毕设演示用不到那么多。我一般会保留六张t_community小区、t_building楼栋、t_owner住户、t_repair报修工单、t_fee缴费记录、t_admin管理员。住户和楼栋是多对一报修和缴费都挂在住户下面。建表时统一加create_time和update_time答辩时老师问「数据什么时候改的」你能答上来。CREATE TABLE t_owner ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 住户姓名, phone VARCHAR(16) NOT NULL COMMENT 手机号登录用, building_id INT NOT NULL COMMENT 所属楼栋, room_no VARCHAR(16) NOT NULL COMMENT 房号, status TINYINT DEFAULT 1 COMMENT 1正常 0迁出, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明phone加唯一索引因为小程序端用手机号做登录标识重复注册会直接报错。status用TINYINT而不是BOOLEAN方便以后扩展成多状态。字符集用utf8mb4住户姓名里可能有生僻字utf8存不下。外键我一般不在数据库层加而是在 Service 层校验因为加了外键后删除楼栋会报错演示时容易卡住。3.2 MyBatis Mapper 与分页查询住户列表是小程序端和管理端都会用到的接口必须支持分页。MyBatis 本身不带分页常见做法是用 PageHelper 插件或者手写LIMIT。毕设里我倾向手写因为少一个依赖答辩时也讲得清楚。!-- OwnerMapper.xml 片段 -- select idselectByPage resultTypecom.example.entity.Owner SELECT o.*, b.building_name FROM t_owner o LEFT JOIN t_building b ON o.building_id b.id WHERE o.status 1 if testkeyword ! null and keyword ! AND (o.name LIKE CONCAT(%, #{keyword}, %) OR o.phone LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY o.create_time DESC LIMIT #{offset}, #{pageSize} /select逻辑说明LEFT JOIN保证即使楼栋被删了住户记录还能查出来只是building_name为空。if做动态 SQL关键字为空时不拼条件。offset和pageSize由 Service 层算好传进来offset (pageNum - 1) * pageSize。注意#{}是预编译占位符能防 SQL 注入如果写成${}小程序端传个1 OR 11就能把整张表拖走这是毕设答辩常被问的安全点。对应的 Mapper 接口和 Service 方法public interface OwnerMapper { ListOwner selectByPage(Param(offset) int offset, Param(pageSize) int pageSize, Param(keyword) String keyword); int countByKeyword(Param(keyword) String keyword); } Service public class OwnerServiceImpl implements OwnerService { Autowired private OwnerMapper ownerMapper; Override public PageResultOwner list(int pageNum, int pageSize, String keyword) { int offset (pageNum - 1) * pageSize; ListOwner list ownerMapper.selectByPage(offset, pageSize, keyword); int total ownerMapper.countByKeyword(keyword); return new PageResult(list, total, pageNum, pageSize); } }参数说明Param注解不能省否则 MyBatis 在 XML 里拿不到参数名。PageResult是自己封装的返回对象包含list、total、pageNum、pageSize四个字段小程序端拿到total才能算总页数。pageSize建议限制在 10 到 20 之间太大一次传几百条小程序渲染会卡。3.3 Controller 层接口与统一返回格式小程序端不认 JSP只认 JSON。所以 Controller 全部用ResponseBody或者RestController返回统一的Result对象。统一格式的好处是前端只写一次拦截逻辑code为 200 取data其他情况弹提示。RestController RequestMapping(/api/owner) public class OwnerController { Autowired private OwnerService ownerService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword) { if (pageSize 50) { return Result.error(每页最多50条); } return Result.success(ownerService.list(pageNum, pageSize, keyword)); } }逻辑说明RequestParam的defaultValue保证不传参时也有默认值小程序端首次加载可以不传pageNum。pageSize做上限校验防止有人手动改 URL 拉全表。Result里code用 200 表示成功500 表示业务失败401 表示未登录前端根据这三个值做分支就够了。4. 微信小程序端对接 SSM 接口的完整流程4.1 请求封装与域名配置微信小程序不允许直接请求localhost开发阶段要在开发者工具里勾选「不校验合法域名」真机预览时则必须用备案过的 HTTPS 域名。毕设演示一般用开发者工具就够但如果你要发给老师试用这一步绕不开。我一般把请求封装成一个request.js统一处理header、token和错误提示。// utils/request.js const BASE_URL https://your-domain.com/api; 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, token: wx.getStorageSync(token) || }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.redirectTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };逻辑说明BASE_URL抽成常量换环境时只改一处。token从本地缓存取登录后写入退出时清除。code为 401 时直接跳登录页不用每个页面单独判断。wx.showToast的icon设为none因为默认的success图标带对勾报错时显示对勾很违和。4.2 住户列表页的分页加载与下拉刷新小程序列表页的标准做法是onLoad加载第一页onReachBottom加载下一页onPullDownRefresh重置到第一页。这里有个高频坑快速上拉会触发多次请求导致同一页数据重复。解决办法是加一个loading标志位。// pages/owner/list.js const { request } require(../../utils/request); Page({ data: { list: [], pageNum: 1, total: 0, loading: false, keyword: }, onLoad() { this.loadData(); }, onReachBottom() { if (this.data.list.length this.data.total) return; this.loadData(); }, onPullDownRefresh() { this.setData({ pageNum: 1, list: [] }); this.loadData().then(() wx.stopPullDownRefresh()); }, loadData() { if (this.data.loading) return Promise.resolve(); this.setData({ loading: true }); return request({ url: /owner/list, data: { pageNum: this.data.pageNum, pageSize: 10, keyword: this.data.keyword } }).then(res { this.setData({ list: this.data.list.concat(res.list), total: res.total, pageNum: this.data.pageNum 1, loading: false }); }).catch(() this.setData({ loading: false })); } });参数说明loading在请求发出前设为true返回后设回falseonReachBottom里先判断loading再决定是否发请求。list.concat而不是直接赋值保证新数据追加在后面。onPullDownRefresh里把pageNum重置为 1、list清空再重新加载最后调wx.stopPullDownRefresh收起刷新动画。total用来判断是否还有下一页list.length total时直接返回避免无效请求。4.3 报修工单提交与图片上传报修功能是小程序端的亮点住户拍照上传后端存图片路径。微信小程序的图片上传用wx.chooseImage加wx.uploadFile注意uploadFile不走request封装要单独写。// 提交报修 submitRepair() { const that this; wx.chooseImage({ count: 3, success(res) { const tempPaths res.tempFilePaths; // 先上传图片拿到路径数组 const uploadTasks tempPaths.map(path new Promise((resolve, reject) { wx.uploadFile({ url: https://your-domain.com/api/upload, filePath: path, name: file, header: { token: wx.getStorageSync(token) }, success(r) { resolve(JSON.parse(r.data).data); }, fail: reject }); })); Promise.all(uploadTasks).then(urls { return request({ url: /repair/add, method: POST, data: { content: that.data.content, images: urls.join(,) } }); }).then(() { wx.showToast({ title: 提交成功 }); wx.navigateBack(); }); } }); }逻辑说明count: 3限制最多选三张避免一次传太多图拖慢上传。Promise.all等所有图片上传完再提交工单保证images字段完整。后端/upload接口把文件存到服务器磁盘返回可访问的 URL数据库里只存 URL 字符串不存二进制。注意wx.uploadFile返回的res.data是字符串必须JSON.parse一次这是新手最容易漏的一步。5. 部署、联调与常见问题排查5.1 本地联调时小程序请求不到后端现象开发者工具里请求一直转圈控制台报request:fail。原因通常是三个一是没勾选「不校验合法域名」二是后端没启动或者端口不对三是BASE_URL写成了localhost但手机和电脑不在同一网段。解决开发阶段在开发者工具「详情-本地设置」里勾选不校验确认 Tomcat 启动日志没有报错真机调试时把BASE_URL换成电脑的局域网 IP比如http://192.168.1.100:8080/api并确保防火墙放行 8080 端口。5.2 中文乱码从数据库一路乱到小程序现象住户姓名在数据库里是好的但小程序端显示成问号或方块。原因Tomcat 的URIEncoding没配或者 MySQL 连接串没加characterEncodingutf8。解决在server.xml的Connector标签加URIEncodingUTF-8JDBC URL 加useUnicodetruecharacterEncodingutf8数据库和表的字符集统一用utf8mb4。三处都改完重启乱码基本消失。5.3 事务不生效导致缴费记录和住户余额不一致现象缴费接口里先插缴费记录再更新住户余额中间抛异常后记录插进去了但余额没变。原因Service 类没有加Transactional或者加了但方法不是public或者异常被catch吞了没抛出去。解决在 Service 实现类的方法上加Transactional(rollbackFor Exception.class)注意rollbackFor要写Exception.class因为 Spring 默认只回滚RuntimeExceptioncatch块里如果要处理异常记得最后throw出去否则事务管理器感知不到。5.4 MyBatis 的#{}和${}用混导致 SQL 注入现象关键字搜索接口传 OR 11能把所有住户查出来。原因XML 里用了${keyword}做字符串拼接。解决一律用#{}它在底层是PreparedStatement的占位符。只有像ORDER BY ${column}这种列名动态拼接才用${}但列名必须来自代码里的白名单不能直接取用户输入。5.5 小程序端setData频繁调用导致页面卡顿现象列表加载几十条后上拉越来越卡。原因每次onReachBottom都setData整个list数据量大了之后视图层和逻辑层通信开销很高。解决分页加载时只concat新数据不要每次重新赋值全量如果单页数据超过 50 条考虑用wx:key优化列表渲染wx:key绑定唯一id而不是索引否则删除或更新时会出现错位。6. 把这套系统讲成答辩加分项的几个技巧答辩时老师最常问的三个问题是你的权限怎么做的、事务在哪控制的、小程序怎么保证登录态。前两个前面已经覆盖这里重点说登录态。常见做法是登录成功后后端生成一个 UUID 作为 token存进 Redis 或者数据库的t_token表设置 2 小时过期小程序端把 token 存进wx.setStorageSync每次请求带在 header 里后端写一个拦截器校验 token 有效性无效返回 401。这套逻辑不复杂但你要能说清楚「为什么不用 session」——因为小程序没有 cookiesession 依赖 cookie 传递 JSESSIONID跨端场景下 token 更合适。另一个加分点是数据校验。不要只在前端做校验后端也要做。比如手机号用正则^1[3-9]\\d{9}$校验房号限制长度报修内容不能为空。我一般会在 Controller 里用Valid加注解做基础校验复杂规则放 Service。答辩时你可以现场演示前端把手机号改成123提交后端返回「手机号格式不正确」这比你说一百句「我做了校验」都有说服力。最后一个技巧是关于演示数据的。提前在数据库里插 20 条住户、10 条报修、5 条缴费记录不要只留一两条。演示分页时能翻页演示搜索时能搜出结果演示统计时图表有数据。我见过太多同学系统功能都写了但数据库是空的老师一看就觉得没跑通过。另外把README.md写清楚环境要求、启动步骤、默认账号密码、接口列表。这份文档在答辩时就是你的后悔药老师问什么你都能翻到对应位置。这套 SSM 加微信小程序的小区管理系统技术栈不新但胜在结构完整、每一层都能讲出所以然。把表设计好、接口调通、小程序端跑顺再补上权限和校验毕设拿个良好以上不难。希望帮到你。本文还有配套的精品资源点击获取