简介:面向需要开发房屋租赁管理系统的开发者、在校学生及项目实践者,这份代码资源基于微信小程序、Spring Boot、uniapp与Vue技术栈,提供了从后端接口到前端页面的完整实现,覆盖用户信息、图片素材、视频素材等核心管理模块,能够帮助快速搭建一个可运行的房屋租赁管理系统原型。资源压缩包共包含1146个文件,约16.04MB,其中java、xml、properties等为Spring Boot后端与MyBatisPlus持久层代码,vue、js、css等为后台管理页面,wxml、wxss、js等则对应微信小程序端,png、svg、jpg等为界面素材,另有sql数据库脚本、bat启动脚本及docx说明文档,整体结构清晰,便于按模块查阅与二次开发。当前已有454人学习下载,适合作为毕业设计、课程实训或企业项目初期的参考蓝本。通过阅读源码,可以掌握小程序与后端API的联调方式、MySQL数据库表设计思路以及图片和视频等素材管理功能的实现细节,对完整理解前后端分离项目的开发流程具有较高的参考价值。
1. 房屋租赁小程序不是“套模板”,是前后端两套工程的对齐
“房屋租赁管理系统微信小程序代码”这个标题看起来像是一个打包好的资源,但真正落到开发上,它意味着两套工程:一套是 SpringBoot 写的后端服务,负责房源、租客、合同、账单的存储和计算;另一套是 uniapp 写的小程序前端,负责在微信里展示房源、提交订单、在线签约和支付。单独会其中一个不难,难的是把两套东西在一个项目里跑通,并且让小程序端的登录态、请求封装和后端的权限拦截完全对齐。这篇文章不评价任何现成源码的质量,只讲我按这个标题做项目时一定会用的技术选型和落地步骤:SpringBoot 提供 API,uniapp 负责跨端页面,微信小程序作为最终交付载体。适合刚接手这类毕设或外包项目、想搞清楚“前后端怎么衔接”的开发者。
2. SpringBoot 后端:房源、订单、账单的接口设计与 JWT 登录
2.1 先定依赖:starter-web 之外,还要加拦截器、JWT 和 MyBatis-Plus
房屋租赁系统后端不是只写 CRUD,租客、房东、合同、账单之间有关联,所以项目骨架我一般用 SpringBoot 2.7.x 配 JDK 8,稳定且兼容大多数云服务器。pom.xml里除了spring-boot-starter-web和spring-boot-starter-validation,重点加下面三个东西:
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.19.2</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>JWT 用java-jwt而不是jjwt,是因为它的 API 更直白,create()和verify()一眼能看懂;MyBatis-Plus 负责单表 CRUD,减少写xml的时间。这里要注意:如果标题对应的项目带mybatis而不是mybatis-plus,也不要慌,BaseMapper换成手写@Select注解同样能跑,但分页和条件查询会繁琐一些。
application.yml里除了数据源,还要把 JWT 的密钥和过期时间提出来:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/house_rental?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: house-rental-secret-key-please-change expire: 604800 # 7天,单位秒逻辑删除字段统一叫deleted,这样房源下架、合同作废不会物理删数据,后续对账和审计有追溯。StdOutImpl会打印每一条 SQL,联调时能直接看到 MyBatis-Plus 生成的语句,排查字段名对不上的问题很有用。
2.2 用 MyBatis-Plus 建三张核心表:房源、租客、合同
租赁系统的核心表不是用户表,而是房源表。房源信息和房东用户分开,租客通过合同关联到房源。建表 SQL 我一般这样设计:
CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, landlord_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, address VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-待出租 1-已出租 2-下架', cover_url VARCHAR(500), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ); CREATE TABLE contract ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, monthly_rent DECIMAL(10,2) NOT NULL, deposit DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-生效中 1-已到期 2-已退租', deleted TINYINT DEFAULT 0 );house.status和contract.status不能靠前端传,后端必须根据当前日期算。比如租客在小程序里看到“已出租”的房源,应该置灰,这个判断放在查询接口里做,而不是让前端过滤。MyBatis-Plus 实体类对应这些表,HouseMapper继承BaseMapper<House>后,基础方法就都有了。但联表查询比如“查某房东名下的所有合同和对应房源”还是要写一个自定义方法:
@Select("SELECT c.*, h.title AS house_title FROM contract c " + "LEFT JOIN house h ON c.house_id = h.id " + "WHERE c.landlord_id = #{landlordId} AND c.deleted = 0") List<ContractVO> selectContractsByLandlord(@Param("landlordId") Long landlordId);ContractVO里比实体类多一个houseTitle字段,这样前端列表页不用再发一次请求拿房源标题。相似的联表都放在Mapper里用注解写,比xml文件少一次切换,适合中小型项目。
2.3 JWT 登录与角色鉴权:租客和房东不能共用一个接口
租赁系统有两种角色:租客和房东。一个用户可能既是租客又是房东,所以用户表要留role字段,但更灵活的做法是登录时返回角色列表,接口用@RequireRole注解做拦截。我这里用更常见的方案:User表有role字段,值为TENANT或LANDLORD,登录接口签发 JWT 时把role塞进去。
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } String token = JWT.create() .withClaim("userId", user.getId()) .withClaim("role", user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() + jwtExpire * 1000)) .sign(Algorithm.HMAC256(jwtSecret)); return Result.ok(new LoginVO(token, user.getRole(), user.getNickname())); }拦截器里先验token,再把userId和role放回ThreadLocal或 request attribute,供Controller拿当前用户。角色校验放在拦截器之后的一层:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BizException(401, "未登录"); } try { DecodedJWT jwt = JWT.require(Algorithm.HMAC256(jwtSecret)).build().verify(token.substring(7)); request.setAttribute("userId", jwt.getClaim("userId").asLong()); request.setAttribute("role", jwt.getClaim("role").asString()); return true; } catch (JWTVerificationException e) { throw new BizException(401, "登录已过期"); } }关键点是Authorization头用Bearer前缀,uniapp 端封装请求时也要带上同样的前缀,少了这个前缀就会被拦截器拒绝。这里踩坑最多的是“为什么后端没报错但前端 401”,多半是请求头名字写错或大小写不一致。
2.4 合同状态机与账单:已退房就不能再生成新租金
我见过很多租赁系统把账单做成“下单时生成一次性费用”,但真实租赁是周期性计费。合同生效期间,每隔一个月生成一条租金账单。这个逻辑不能写在小程序端,必须后端定时任务控制。SpringBoot 里用@Scheduled注解就能做:
@Scheduled(cron = "0 0 2 * * ?") public void generateMonthlyBills() { List<Contract> activeContracts = contractMapper.selectActiveContracts(); for (Contract contract : activeContracts) { Bill bill = new Bill(); bill.setContractId(contract.getId()); bill.setHouseId(contract.getHouseId()); bill.setAmount(contract.getMonthlyRent()); bill.setPeriod(YearMonth.now().toString()); bill.setStatus(0); if (billMapper.selectExists(contract.getId(), bill.getPeriod()) == null) { billMapper.insert(bill); } } }selectExists是自定义方法,查contract_id + period是否已有账单,防止定时任务重复执行。这里的cron是每天凌晨 2 点跑一次,YearMonth.now()得到的是“当月”,表示生成当月租金。退租时合同状态改成已退租,selectActiveContracts只查status = 0的合同,这样已退房不会继续生成账单。这套状态机是房屋租赁系统最值得花时间设计的地方,比页面多一个v-for重要得多。
3. uniapp 微信小程序端:页面结构、request 封装与登录态
3.1 HBuilderX 创建 uniapp 项目,并确认小程序目录结构
标题里写的是“微信小程序代码”,但用 uniapp 开发时,代码是.vue文件,而不是原生小程序的.wxml。在 HBuilderX 里选择“文件 - 新建 - 项目 - uni-app”,模板选“默认模板”,然后manifest.json里配置mp-weixin的appid:
{ "mp-weixin": { "appid": "你的小程序AppID", "setting": { "urlCheck": false }, "usingComponents": true } }本地开发时urlCheck设为false可以跳过合法域名校验,但真机预览时微信开发者工具会警告。我一般保留false只用于开发,上线前改成true。pages.json里的pages数组决定小程序有哪些页面,第一个页面是首页,租赁系统通常首页是房源列表,所以pages/index/index要放在第一位。tabBar 用“首页、签约、我的”三个项就够,不建议放太多。
3.2 request.js 封装:baseURL、token、错误码统一处理
小程序没有axios,uniapp 自带uni.request,但它不处理 token 过期,也不统一错误提示。我一般封装一个request.js,放在utils目录下:
const BASE_URL = 'http://localhost:8080' export function request(options) { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }, success: (res) => { if (res.statusCode === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) reject(new Error('未登录')) return } if (res.data.code !== 0) { uni.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) return } resolve(res.data.data) }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }BASE_URL在本地开发时写http://localhost:8080,真机预览时不能写localhost,要写电脑的局域网 IP,比如http://192.168.1.100:8080。这里特别容易踩坑:真机上的小程序不能访问localhost,那指的是手机自己。code字段是后端返回的业务状态码,0表示成功,和后端Result类保持一致。登录失效统一跳转登录页,而不是每个页面自己写一遍。
3.3 首页房源列表:分页加载与下拉刷新
房源列表是租赁小程序最核心的页面。用onLoad生命周期里请求第一页,onReachBottom加载下一页:
export default { data() { return { page: 1, pageSize: 10, houseList: [], hasMore: true } }, onLoad() { this.loadHouseList() }, onReachBottom() { if (this.hasMore) { this.page++ this.loadHouseList() } }, methods: { async loadHouseList() { try { const list = await request({ url: '/api/house/list', method: 'GET', data: { page: this.page, pageSize: this.pageSize } }) if (list.length < this.pageSize) this.hasMore = false this.houseList = this.page === 1 ? list : this.houseList.concat(list) } catch (e) { console.error(e) } } } }后端的分页接口要返回total和records,这样前端可以用total来判断是否还有下一页,而不是用list.length < pageSize。上面的写法是简化版,实际我会在request里返回整个分页对象,data结构为{ records: [], total: 100 }。下拉刷新用onPullDownRefresh,在方法里重置page = 1再调用loadHouseList,最后uni.stopPullDownRefresh()关闭动画。
3.4 小程序登录:wx.login 换 code,后端换 openid
微信小程序登录不能直接用用户名密码,而是先wx.login拿到临时code,发给后端,后端拿code调微信接口换openid。uniapp 里uni.login封装了这一步:
uni.login({ provider: 'weixin', success: async (loginRes) => { const res = await request({ url: '/api/auth/login', method: 'POST', data: { code: loginRes.code, nickname: '', avatar: '' } }) uni.setStorageSync('token', res.token) uni.setStorageSync('userInfo', res.userInfo) } })后端收到code后,需要调用微信的jscode2session接口,这一步要发起 HTTP 请求,可以在 Java 中用RestTemplate或OkHttp:
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; RestTemplate rt = new RestTemplate(); String result = rt.getForObject(url, String.class); // 解析 result 得到 openid 和 session_key注意这里的appid和secret是小程序自己的,不是后端项目的配置文件里随便写的。拿到openid后查用户表,没查到就自动注册一个新用户,然后签发 JWT。这个流程里最容易出错的是code只能用一次,后端拿到后必须立刻换openid,如果前端反复提交就会报invalid code。
4. 前后端联调:本地开发、真机预览与常见的跨域/域名问题
4.1 开发环境三件套:局域网 IP、关闭校验、CORS 配置
联调时先保证后端能通。SpringBoot 默认只允许同源访问,小程序端和本地后端是不同端口,必须配 CORS:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }然后在微信开发者工具里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,manifest.json里urlCheck也设为false。这样本地localhost:8080和局域网 IP 都能访问。真机预览时,手机和电脑必须在同一个 WiFi 下,然后后端启动时要监听0.0.0.0,SpringBoot 默认就是监听所有网卡,但云服务器或防火墙要放行 8080 端口。
4.2 微信支付 v3 在租赁场景下的接入要点
房屋租赁涉及押金、租金,微信支付逃不掉。小程序端调用uni.requestPayment前,需要先拿到后端生成的支付参数。后端接入微信支付 v3 时,要准备商户号、API 证书序列号和私钥。这里不说完整代码,只说最关键的签名和调起:
// 后端统一下单成功后,返回小程序所需参数 Map<String, String> payParams = new HashMap<>(); payParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); payParams.put("nonceStr", UUID.randomUUID().toString().replace("-", "")); payParams.put("package", "prepay_id=" + prepayId); payParams.put("signType", "RSA"); // sign 需要按微信规则用商户私钥签名 payParams.put("paySign", buildSign(payParams));小程序端拿到这些参数后:
uni.requestPayment({ provider: 'wxpay', timeStamp: res.timeStamp, nonceStr: res.nonceStr, package: res.package, signType: 'RSA', paySign: res.paySign, success: () => { uni.showToast({ title: '支付成功' }) } })注意package的值是字符串prepay_id=xxx,不能只传xxx。支付成功后不要立刻把订单状态改成“已支付”,要以微信支付回调为准,回调里更新账单状态。这里有个常见坑:支付金额以自然货币单位分传递,后端返回给前端的payParams里不涉及金额,但统一下单接口里金额必须传int类型的分,不能传double元。
4.3 从“请求失败”到“数据不对”的排查清单
联调时我按这个顺序查问题,能省一半时间:
| 现象 | 检查点 | 常见原因 |
|---|---|---|
| 请求直接失败,无返回 | Network 面板看请求是否发出 | urlCheck未关闭、baseURL 写错 |
| 返回 404 | 后端 Controller 路径 | @RequestMapping前缀不一致 |
| 返回 401 | 请求头是否带Bearer | 拦截器没放行登录接口 |
| 返回数据但页面不显示 | 字段名大小写 | Java 驼峰 vs 前端下划线 |
| 数据有但分页重复 | hasMore逻辑 | 没重置page |
最隐蔽的问题是 Java 后端返回的userId是Long类型,超过Number.MAX_SAFE_INTEGER时 JS 会丢失精度。解决方法是后端把这类 id 转成String返回,或者在application.yml里配置Jackson对Long转字符串:
jackson: generator: write-numbers-as-strings: true但这个配置会把所有数字都变成字符串,如果不想影响其他数字字段,就在实体类的id字段上加@JsonSerialize(using = ToStringSerializer.class)。
4.4 上线前必须改的三处配置
第一,BASE_URL要换成已备案的 HTTPS 域名,且该域名必须在小程序后台配置为 request 合法域名。第二,manifest.json的mp-weixin里urlCheck改回true,或者干脆去掉这个字段,微信开发者工具上传时会按后台配置校验。第三,JWT 密钥要换掉本地开发用的默认值,jwt.secret至少 32 位,并且不要提交到 Git 仓库。
5. 房屋租赁特有的状态同步技巧:用定时任务 + 乐观锁处理“到期自动退租”
租赁系统和其他商城系统最大的不同是“时间驱动”:合同到期、账单逾期、房源状态恢复,这些都不能等用户操作,必须系统自己变。我常用的技巧是写一个RentalStatusTask,每半小时扫一次合同表:
@Scheduled(cron = "0 30 * * * ?") public void syncContractStatus() { // 找出所有生效中且结束日期小于今天的合同 List<Contract> expired = contractMapper.selectExpiredContracts(new Date()); for (Contract contract : expired) { int updated = contractMapper.updateStatusWithVersion( contract.getId(), 1, // 已到期 contract.getVersion() ); if (updated > 0) { houseMapper.updateStatus(contract.getHouseId(), 0); // 房源恢复可租 } } }这里的updateStatusWithVersion是带有乐观锁的更新:
@Update("UPDATE contract SET status = #{status}, version = version + 1 " + "WHERE id = #{id} AND version = #{version}") int updateStatusWithVersion(@Param("id") Long id, @Param("status") Integer status, @Param("version") Integer version);为什么加version而不是直接UPDATE contract SET status = 1 WHERE id = ??因为租客可能在到期前几分钟发起退租,后端把状态改成了已退租(status=2),定时任务再用status=1覆盖就错了。乐观锁保证只有version没变时才更新,先到先得。如果updated > 0再改房源状态,否则什么都不做,下轮任务再扫。
验证这个技巧是否生效,可以直接改数据库把合同结束日期设为昨天,等下一个半小时周期观察状态变化。也可以手动调用一次任务接口,但生产环境建议把@Scheduled注释掉,用运维平台手动执行。另一个技巧是账单逾期要发提醒,不要在for循环里发消息,而是先把逾期合同 id 收集起来,批量调用微信订阅消息接口,这能避免触发微信的接口频率限制。这套“定时扫描 + 乐观锁更新 + 批量通知”的思路,覆盖了租赁系统里 80% 的状态变更场景,比在控制器里写一堆 if else 可靠得多。
本文还有配套的精品资源,点击获取