ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM+微信小程序房屋租赁系统:从数据库设计到权限拦截

SSM+微信小程序房屋租赁系统:从数据库设计到权限拦截 简介面向计算机类专业毕业设计基于微信小程序、SSM后端与MySQL的房屋租赁管理系统提供了完整业务闭环管理员端的个人中心、用户管理、房屋类型与房源管理中介端的房屋信息、租房订单管理以及用户端的房屋浏览、账单查询等。资源包共891个文件主流格式包括Java业务代码、Vue管理后台、SQL数据库脚本、微信小程序wxml/wxss页面以及用于展示和部署的png图片、mp4视频教程整体压缩后为59.15MB目录按前端、后端、数据库和文档分模块。除源代码外附带数据库初始化脚本和论文视频能帮助快速搭建环境、理解SSM框架与小程序交互过程。该资料已被116人学习适合毕业设计或课程设计参考兼顾功能完整性与教学演示价值。1. 从 SSM 到微信小程序房屋租赁系统的角色边界与订单流做租房管理系统最常见的问题不是写不出接口而是把管理员、中介、用户三种角色揉在一个页面里。这套基于微信小程序 SSM MySQL 的房屋租赁系统比较适合做毕业设计或者给小型中介快速搭一个管理闭环。优势在于小程序端只承担看房、下单、查账单后端把订单、账单、房源、用户权限拆开线上线下对账就清楚。后端里同时还有一套浏览器用的运营后台从工程里的IndexAsideStatic.vue.bak能看出前端经历了多版改动用来给管理员和中介录入房源、审核订单。我不会只罗列功能会从数据库脚本、SSM 分层、小程序接口联调和本地启动脚本四块拆最后给一个用来答辩和验收的数据一致性查询技巧。2. 数据库脚本与 SSM 分层订单、账单、房源字段怎么对齐2.1 从.bak文件和 bat 脚本反推项目结构拿到源码包后先不要急着导入 IDE先看文件后缀。工程里出现了main.css.bak、update-password.vue.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak这说明后台管理端是用 Vue 写的.bak是改版前的备份。有些同学一看到.bak就删掉会导致某些样式或组件版本回退不了。我的习惯是保留.bak先对比原文件和备份的差异确认现在的版本是我们要的。后端部分从.classpath和org.eclipse.wst.common.component能判断这是 Eclipse WTP 的 Java Web 工程。SSM 项目的包结构通常是这样的src/main/java com.xxx.controller com.xxx.service com.xxx.mapper com.xxx.entity src/main/resources jdbc.properties mybatis-config.xml spring-mvc.xml spring-mybatis.xml src/main/webapp如果是 Maven 工程pom.xml里基本是spring-webmvc、mybatis、mysql-connector-java、druid或c3p0依赖。这里不是让你照搬目录而是让你明白这套代码的入口在哪。后端的启动入口不是Application.java而是配置在web.xml里的 Spring 监听器和DispatcherServlet。先找到这两个文件启动流程就清楚了一半。2.2 数据库脚本核心表结构与字段说明数据库脚本是整个系统的地基。管理员、中介、用户三种角色对应不同的表但租房的业务流是连在一起的。我见过不少人把订单和账单混在一张表里结果月底对账时非常痛苦。这套系统的表拆分相对合理至少包含以下角色和业务表表名作用关键字段sys_admin后台管理员admin_id, account, passwordagent中介agent_id, name, phone, statususer_info小程序用户user_id, openid, nickname, phonehouse_info房屋信息house_id, agent_id, title, price, statushouse_type房屋类型type_id, type_namehouse_source房源管理source_id, house_id, source_statusrent_order租房订单order_id, user_id, house_id, amount, statusrent_info租房信息租约rent_id, order_id, start_date, end_daterent_bill租房账单bill_id, rent_id, fee, pay_status这个表结构的关键点在于rent_order只是“意向订单”真正确认入住后由rent_info记录租约周期再由周期生成rent_bill。三张表不要合并否则后续做逾期提醒和季度统计时会不停地在GROUP BY上叠加判断条件。数据库脚本导入时我建议直接用 MySQL 8.0但要看连接串是否兼容。如果脚本里有ENGINEInnoDB DEFAULT CHARSETutf8导入后还要确认排序规则。一段常见的建表 SQL 如下CREATE TABLE rent_order ( order_id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 下单用户, house_id bigint(20) NOT NULL COMMENT 房屋, agent_id bigint(20) DEFAULT NULL COMMENT 负责中介, amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 订单金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已取消 3已完成, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id), KEY idx_user_house (user_id, house_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里要注意三点。第一金额用decimal(10,2)不能用float否则频繁累加后精度会漂移。第二order_no要作为业务唯一标识在 Java 层生成而不是依赖数据库自增方便与微信支付或线下收款对账。第三联合索引idx_user_house能同时撑起“用户订单列表”和“某房屋的订单记录”两种查询单独对user_id建索引反而浪费空间。导入脚本时用命令行更可控。最常见的误导是直接在图形工具里执行整个脚本报错后只提示某一行问题很难定位。我一般这样执行mysql -u root -p --default-character-setutf8mb4 -e CREATE DATABASE IF NOT EXISTS house_rent DEFAULT CHARSET utf8mb4; mysql -u root -p --default-character-setutf8mb4 house_rent house_rent.sql如果脚本里已经包含了CREATE DATABASE第一行可以省略。--default-character-setutf8mb4是为了避免 Windows 下source命令默认以gbk解析导致中文字段名或备注乱码。导入完成后用show tables;和desc rent_order;验证关键表是否都在。2.3 SSM 分层Controller、Service、Mapper 的典型写法SSM 框架这么多年核心套路基本固定。Controller 只做参数接收和结果封装Service 写业务判断Mapper 直接用接口绑定 XML 里的 SQL。以“查询租房订单列表”为例Controller 层可以这样写RestController RequestMapping(/order) public class RentOrderController { Autowired private RentOrderService rentOrderService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer status) { return Result.ok(rentOrderService.pageByStatus(pageNum, pageSize, status)); } }RequestParam里defaultValue 1是为了兼容小程序端第一次请求不带页码的情况。status允许为空这样列表页可以展示全部状态。注意不要用PathVariable传这些查询参数因为小程序端很多请求不是 RESTful 路由传参。Service 层要把分页和状态判断写在一起而不是把PageHelper的调用散落在 ControllerOverride public PageInfoRentOrderVO pageByStatus(Integer pageNum, Integer pageSize, Integer status) { PageHelper.startPage(pageNum, pageSize); ListRentOrderVO list rentOrderMapper.selectOrderWithHouse(status); return new PageInfo(list); }PageHelper.startPage之后的第一个查询会被拦截并加上LIMIT。这段代码的逻辑是先把分页参数交给PageHelper再执行 Mapper 查询最后返回PageInfo。如果你在startPage和查询之间做了一次其他数据库操作分页就会加到那条 SQL 上这是比较隐蔽的坑。Mapper 的 XML 里通常会做连表查询把订单、房屋标题、用户昵称一次查出来select idselectOrderWithHouse resultTypecom.xxx.entity.vo.RentOrderVO SELECT o.order_id, o.order_no, o.amount, o.status, h.title AS houseTitle, u.nickname AS userName FROM rent_order o LEFT JOIN house_info h ON o.house_id h.house_id LEFT JOIN user_info u ON o.user_id u.user_id where if teststatus ! null AND o.status #{status} /if /where ORDER BY o.create_time DESC /select这里用LEFT JOIN是为了防止房屋或用户被误删后订单查不出来。resultType直接映射到RentOrderVO不用在 Java 里再手动拼接字段。注意RentOrderVO里的属性名是houseTitle不是house_title因为 MyBatis 默认会自动开启驼峰映射前提是mybatis-config.xml里设置了mapUnderscoreToCamelCasetrue。3. 小程序登录、房源列表与管理员后台的接口联调3.1 小程序端目录结构与请求封装小程序端一般就是原生微信小程序或者用 uni-app 套壳。这套系统里如果看到pages/house/list、pages/order/detail这类目录基本可以确定是原生小程序。原生的好处是跑起来快不用提前编译到 H5坏处是每个页面的onShow要自己控制加载状态。无论是哪种第一件事就是把wx.request封装起来。不要在每个页面里直接写wx.request否则后端接口地址一变你就要全局搜索。我一般会在utils/request.js里放一个封装const BASE_URL http://localhost:8080/rent; function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, token: wx.getStorageSync(token) }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) reject(err) }); }); } module.exports { request, BASE_URL };这段封装里最重要的不是BASE_URL而是token从本地缓存读取并放进请求头。很多 SSM 项目的登录是用session维持的但小程序端的wx.request在真机上对Cookie的支持并不稳定所以后端要改成 token 模式。具体做法是登录后生成一个 token用Redis或内存Map保存小程序每次请求带着它。还需要注意真机调试时http://localhost指向的是手机自身不是你的电脑。这里需要在小程序开发者工具里勾选“不校验合法域名”并且把BASE_URL改成电脑的局域网 IP或者用内网穿透工具。但这属于联调配置不是业务代码。3.2 微信登录code2session 换取 openid 生成 token微信登录的流程是固定的小程序调用wx.login拿到code把code传给后端后端调用微信的code2session接口换取openid和session_key。一定要记住openid才是用户的稳定唯一标识不要拿nickname做关联因为用户可以随时改。后端接收code的 Controller 可以这样写PostMapping(/wx/login) public Result wxLogin(RequestBody MapString, String params) { String code params.get(code); if (StringUtils.isEmpty(code)) { return Result.error(code is empty); } String openid wxService.code2Session(code); UserInfo user userMapper.selectByOpenid(openid); if (user null) { user new UserInfo(); user.setOpenid(openid); user.setCreateTime(new Date()); userMapper.insert(user); } String token UUID.randomUUID().toString().replace(-, ); tokenCache.put(token, user.getUserId()); return Result.ok(Collections.singletonMap(token, token)); }流程拆解先用code向微信服务器换openid然后查本地user_info表。如果没查到就自动注册最后生成一个随机 token 写到缓存里。这里没有讨论session_key因为获取手机号或者解密用户信息时才需要它如果你只是做订单管理openid就够用了。这里的tokenCache可以用ConcurrentHashMap但要注意重启后缓存清空用户要重新登录。更稳妥的方案是把 token 存到 MySQL 或 Redis。在毕业设计场景里如果你不想引入 Redis 增加部署复杂度用Map也能答辩但最好说明生产环境下的替换思路。另外微信小程序的code是一次性的且有效期只有五分钟。如果后端调用code2session报40029多半是同一段 code 被重复使用了或者前端把wx.login放在了onShow里导致每次进页面都换一次 code。这个坑在后面“修改刚进入的加载页面”时特别常见。3.3 房源列表分页与状态字段处理房源列表是用户打开小程序后看到的第一个页面。这里有两个要点一是分页二是状态。分页用后端封装的PageHelper前端通过onReachBottom加载下一页。状态字段是house_info.status0表示上架1表示已下架。用户端只能看到status 0的房源。前端拉取房源列表的代码const { request } require(../../utils/request); Page({ data: { houseList: [], pageNum: 1, pageSize: 10, hasMore: true }, async loadHouses() { const pageNum this.data.pageNum; const res await request(/house/page, GET, { pageNum, pageSize: this.data.pageSize }); const rows res.list || []; this.setData({ houseList: this.data.houseList.concat(rows), hasMore: rows.length this.data.pageSize }); }, onReachBottom() { if (!this.data.hasMore) return; this.setData({ pageNum: this.data.pageNum 1 }); this.loadHouses(); } });这段代码有两个容易踩的细节。一是concat之前要先判断hasMore否则用户在下拉到底后会连续发送多个同页码请求。二是后端接口PageHelper返回的PageInfo里当前页的字段叫pageNum不要跟前端自定义的pageNum混在一起这里命名相同但作用域不同传参时很容易把分页参数覆盖成字符串。关于页面加载状态很多小程序模板在onLoad里直接弹wx.showLoading但房源列表图片多、加载慢弹窗会遮住页面。我的习惯是在页面顶部用一个loading变量控制骨架屏或者简单文本。这与微信小程序顶部导航栏高度那些布局问题无关但都是在真机上常见的体验细节。3.4 角色权限管理员、中介、用户如何区分管理员、中介、用户三种角色不能只靠前端隐藏按钮来实现。你应该在后端接口的调用入口做一次拦截。常见做法是在 Spring 的拦截器里判断请求头里的 token然后从缓存取角色信息。角色权限矩阵可以这样理解后端路径管理员中介用户/house/page可访问可访问可访问/house/delete可访问禁止禁止/order/list全部订单名下订单本人订单/bill/list全部账单名下账单本人账单为了省事很多 SSM 项目会在每个 Mapper 查询里加agent_id条件。中介登录后只能看到自己名下的房源和订单。管理员可以看到全部。这种行级权限适合用 MyBatis 拦截器统一处理但入门阶段我建议先在 Service 层手动校验public void deleteHouse(Long houseId, Integer agentId) { HouseInfo house houseMapper.selectByPrimaryKey(houseId); if (house null) { throw new RuntimeException(房屋不存在); } if (house.getAgentId() ! null !house.getAgentId().equals(agentId)) { throw new RuntimeException(无权操作其他中介的房源); } houseMapper.deleteByPrimaryKey(houseId); }这里的逻辑很直白先查房屋再对比当前登录中介和房屋归属不匹配就抛异常。注意在 SSM 项目里异常抛到前端要经过ControllerAdvice统一处理否则小程序端收到的是一段默认的 Spring 错误页而不是{code: 500, msg: 无权操作}。4. 本地启动与排错install/run/build 三个 bat 的实际用法4.1 环境准备与 bat 脚本逐条拆解很多同学拿到源码包后第一步就是双击1-install.bat然后发现窗口一闪而过。bat 脚本是给 Windows 下的自动化操作准备的源码包里的1-install.bat、2-run.bat、3-build.bat命名很直观但内容大概率是偏个人化的路径配置。不要盲目执行先用编辑器打开看一眼。我见过比较合理的脚本写法是这样的echo off rem 安装依赖与初始化 mvn clean install -DskipTests npm install --prefix ./adminecho off rem 启动后端与后台管理端 mvn spring-boot:run cd admin npm run serveecho off rem 打包生产版本 mvn clean package -DskipTests cd admin npm run build当然如果项目不是 Maven 标准结构而是 Eclipse WTP 工程1-install.bat里可能是把项目发布到 Tomcat 的webapps目录再执行startup.bat。这时候你要先确认本机有没有 Maven、Node.js、Tomcat。版本上SSM 项目一般配 JDK 8前端如果用的 Vue 2Node 版本适合 12-16太新容易openssl报错。4.2 常见启动失败端口占用、MySQL 时区、JDK 版本本地启动最常见的失败场景是 Tomcat 端口被占用。后端启动日志会直接报Java.net.BindException: Address already in use然后在 Windows 下用命令行查占用netstat -ano | findstr 8080 taskkill /F /PID 1234注意taskkill是强制结束找对 PID 很重要。如果你开了两个微信开发者工具的后台进程也有可能会把附近的端口占掉。另一个常见问题是 Spring 的jdbc.properties里数据库连接串没有带时区jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/house_rent?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456MySQL 8.0 以上如果不写serverTimezone会直接报The server time zone value Öйú±ê׼ʱ¼ä之类的错。这个问题在很多 mysql 安装配置教程里都会提到但依旧是最容易被忽略的。同时记得驱动类名要改成com.mysql.cj.jdbc.Driver旧版的com.mysql.jdbc.Driver只能在 MySQL 5.x 下用。JDK 版本问题在 Eclipse 导入项目时特别典型。.classpath文件里如果写的是JavaSE-1.8而你本机装的是 JDK 17解决方式有两种一种是在 IDE 里修改项目的 Java Compiler 和JRE System Library另一种是在pom.xml里更新maven.compiler.source和target。但即便编译过了Tomcat 版本也要匹配JDK 8 配 Tomcat 8.5 或 9JDK 17 配 Tomcat 10 会更省事。如果启动后接口能访问但通过小程序请求时一直转圈优先看开发者工具里的报错若是invalid url多半是BASE_URL不合法若是404说明后端接口路径没对上若是502通常是后端启动失败。这里不建议直接给小程序端把BASE_URL改成https因为本地开发没有证书http加“不校验合法域名”才是常规做法。调试时如果在 IDEA 里打断点提示“当前不会命中断点”先检查编译输出目录是不是旧的 class 文件执行mvn clean后重新构建一般能解决。4.3 数据库脚本导入的坑数据库脚本导入失败的常见原因有三个编码、外键顺序、脚本里多余的注释。先说编码.sql文件保存为 UTF-8 还是 GBK会直接影响字符串字段里的中文是否乱码。更麻烦的是用图形工具导入时软件会按系统默认编码解析可能你看着正常但到了浏览器里全是???。唯一可靠的做法是命令行指定字符集。外键顺序的问题在于项目里如果先创建rent_order再创建house_info而rent_order有外键引用house_info执行到一半就会报错。很多脚本导出时不会自己调整顺序你需要手动查看脚本里的CREATE TABLE顺序。一个快速检查方式grep -n CREATE TABLE house_rent.sql然后用sed -n 20,30p house_rent.sql查看某一段内容确认建表顺序。如果脚本开头有SET FOREIGN_KEY_CHECKS0;那就不用担心但要在最后加SET FOREIGN_KEY_CHECKS1;。检查无误后再执行。还有一个隐蔽的坑脚本里可能包含DROP TABLE IF EXISTS这个命令会静默删除旧表。如果你本机已有同名数据库导入后旧数据会被清空。所以在导入前不要直接source可以先备份已有库mysqldump -u root -p house_rent house_rent_backup.sql这样即使表结构被覆盖本地数据还能恢复。5. 用 SQL 视图与接口拦截把论文设计变成可验收功能5.1 用 SQL 视图验证订单数据一致性在答辩演示或实际上线初检时光看几个后台页面不够还要证明数据是对的。我建议创建一个 SQL 视图把订单、租约、账单的累计金额拉到同一张表里这样一旦某个订单的账单没生成一眼就能看出来。CREATE OR REPLACE VIEW v_rent_order_check AS SELECT o.order_no, o.order_id, o.amount AS order_amount, r.rent_id, COALESCE(SUM(b.fee), 0) AS billed_amount, COUNT(b.bill_id) AS bill_count, CASE WHEN r.rent_id IS NULL THEN 有订单无租约 WHEN SUM(b.fee) IS NULL OR SUM(b.fee) o.amount THEN 账单金额不足 WHEN SUM(b.fee) o.amount THEN 账单金额超出 ELSE 一致 END AS check_result FROM rent_order o LEFT JOIN rent_info r ON o.order_id r.order_id LEFT JOIN rent_bill b ON r.rent_id b.rent_id GROUP BY o.order_no, o.order_id, o.amount, r.rent_id;视图的逻辑拆开看订单表作为主表先关联租约再关联账单。COALESCE处理 null 值避免SUM结果为 null 时被判断成“账单金额不足”。check_result用CASE WHEN输出可读的结果。查询时直接SELECT * FROM v_rent_order_check WHERE check_result ! 一致;就能导出异常数据。这种方式比在后端拼一个 Excel 导出更直观因为它是实时计算当前库里的数据。答辩时可以先手动生成几条脏数据比如把一个订单的账单金额改小然后用这个视图展示检测结果再写一条UPDATE修复。5.2 用 HandlerInterceptor 限制中介“可看不可改”最后说一个在开发验收时经常被追问的细节如何保证中介只能看不能改自己权限外的数据。很多系统只在前端用v-ifrole agent控制按钮别人抓包直接调接口就能越权。我一般会在 Spring 的拦截器里加一个HandlerInterceptorpublic class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Integer role (Integer) request.getAttribute(loginRole); String uri request.getRequestURI(); if (role ! null role 2 uri.contains(/house/delete)) { response.setStatus(403); return false; } return true; } }拦截器的实现思路在登录时把loginRole放进request的 attribute或者从 token 缓存里取出来然后判断 URI。这个方案比较粗暴但对于 SSM 项目足够了。正式一点的做法是在RequestMapping上配合自定义注解在HandlerInterceptor里解析注解权限码然后判断角色是否匹配。需要注意的是如果preHandle返回false响应要设置编码否则小程序端收到的 403 页面是乱码。切换角色后还要验证两件事一是拦截器是否被注册到 interceptor 路径列表里二是不同的请求路径是否都被覆盖。例如/house/delete、/order/refund、/bill/update这类操作请求即使前端没有入口也要在拦截器里拦截掉避免被人拼出 URL 直接访问。本文还有配套的精品资源点击获取
返回列表