
做租房管理系统的同学大概率都经历过这样的场景台账记了一堆租金有没有收到全凭脑子回忆房子到底空了几套还得翻Excel数半天。我当初接手这个“基于JavaWeb和MySQL的SSM房屋租赁管理系统”项目时痛点就是房东这边的房源、租客、合同、账单全乱成一锅粥。这套系统用JavaSSMSpringSpringMVCMyBatis做后端Layui做管理界面MySQL存数据JSP渲染页面典型的学生毕设/课设结构但它麻雀虽小五脏俱全把租赁业务里的核心流程都走通了。这篇文章我会把这个项目的设计思路、表结构、配置文件、核心业务实现全部拆开讲包括我踩过的坑和排查手记无论你是想交差还是真想把这套东西跑起来都能直接对着抄作业。1. 为什么是SSMLayuiJSP而不是别的东西1.1 SSM三件套到底在项目里各干哪些活很多人一开始接触SSM会觉得配置繁琐老想着要不要直接上SpringBoot。但如果你要做毕设或课设选SSM其实是个稳妥的决定——不是说SpringBoot不好而是SSM作为经典组合在网上能找到的资料最全出任何问题都搜得到案例。而且它把Spring的依赖注入、SpringMVC的请求分发、MyBatis的SQL操作这三层分得很清楚天然适合用来理解JavaWeb项目的运行机制。用生活化类比来说SpringMVC是前台接待所有请求先到它手上由它决定找哪个部门的哪个员工处理Spring是后勤总管所有对象Controller、Service、Mapper的生命周期和互相依赖都由它装配缺了它各部门就联系不上MyBatis是专门跑数据库的外勤你给它一句SQL和参数它把数据查回来再封装成对象你写Java代码时基本不用碰JDBC那一堆脏活。具体到房屋租赁这个业务场景分工会变成用户在浏览器点“添加房源”请求到SpringMVC的HouseControllerController调HouseService接口Service里写业务判断比如房号不能重复、状态字段是否正确然后调HouseMapper接口MyBatis在HouseMapper.xml里执行对应的insert语句把数据落到MySQL。我在做这个项目时对分层边界尤其敏感Controller里绝不写SQLMapper里尽量不写业务逻辑Service里不出现HttpServletRequest。规矩一旦定死后面加功能、改Bug就舒服很多尤其是答辩或演示环节被老师突然问“给你加个需求你准备改哪里”你能马上回答出改哪一层。1.2 为什么坚持用JSP而不是前后端分离现在很多教程都在推VueSpringBoot的分离式开发但要注意SSM项目如果强行拆前后端Layui的数据请求方式、JSON返回结构都要重新设计复杂度直接翻倍。JSP的好处就是服务端渲染ModelAndView直接往页面塞数据配合JSTL标签在页面上遍历房源列表逻辑特别直白。以房源列表页为例流程是这样的HouseController的list方法调用Service查询List把结果放进ModelAndView设置视图名称house/listJSP页面用c:forEach把List渲染成表格行。整个过程你脑子里能画出完整的链路出了问题也能顺着链路一步步排查完全不用像前后端分离那样处理跨域、Token、异步渲染那一堆附加问题。当然JSP也有它的毛病比如页面里混入过多Java代码会很难维护。我的经验是JSP里只做循环展示和简单的if判断复杂的计算一律放到Service里处理完再传过来。启动项目时页面还要经过JSP引擎编译成Servlet第一次访问会稍微慢一点这是正常的不用慌。1.3 Layui对后端开发者的友好程度选Layui不是因为它功能最花哨而是它对后端开发者实在友好。它不像ElementUI那样要懂一堆Vue的响应式原理也不像Bootstrap那样全部要靠手搓组件。Layui自带一套完整的模块化前端框架表格、表单、弹层、分页都是现成的你只需要按它的文档把layui.use([‘table’,’form’])引进来写几行table.render就能得到一个带分页、带搜索、带工具栏的表格。在这个项目里我用Layui实现了管理后台的整体框架。左侧菜单是layui的侧边栏导航右侧内容区用iframe嵌入各个JSP页面顶部是当前管理员的信息和退出按钮。整个布局只需要一个admin.jsp页面加上少量JS控制菜单切换工作量比想象中小很多。Layui的数据表格需要后端返回特定格式的JSON大概是{“code”:0,”msg”:””,”count”:100,”data”:[…]}这种结构。刚开始我不懂这个约定返回了普通的List格式结果前端表格一直空白。后来把返回格式改成Layui要求的规范表格立刻渲染出来了。这个细节我在后面还会专门讲。2. 数据库设计是这套系统的心脏2.1 核心表拆解房屋表、租客表、合同表数据库设计直接决定业务逻辑的复杂度。做租赁系统最核心的几张表是房屋信息表、租客信息表、合同表、租金账单表、报修记录表、系统用户表。房屋表house字段我会这么设计id、house_code房号唯一、address、area面积、rent_price月租金、deposit押金、status0空闲、1已租、2维修中、create_time、update_time。这里status字段很关键它直接控制房源能不能被签约前端下拉框筛选也依赖它。租客表tenant字段id、name、phone、id_card身份证号、create_time。注意id_card要做唯一约束因为同一个身份证号可能多次租房历史记录要能查得到不能直接删数据只能做逻辑上的退租处理。合同表contract是连接房屋和租客的桥梁字段包括id、house_id、tenant_id、start_date、end_date、monthly_rent、deposit、status0生效中、1已退租、2已到期、create_time。合同表里冗余了monthly_rent和deposit而不是去关联查询房屋表这样做的好处是如果中途管理员改了房屋租金历史合同不会被影响每一份合同的账单都是独立快照。这里有一个业务经验要分享租房合同的租期重叠校验。同一个小区的同一个房间如果租期时间范围有交叉那肯定是有问题的。我在签约时写的SQL逻辑是select count(*) from contract where house_id#{houseId} and status0 and ((#{startDate} between start_date and end_date) or (#{endDate} between start_date and end_date))。查出来大于0就直接拒绝签约防止一套房租给两个人。2.2 租金账单表的设计思路租金账单表rent_bill字段id、contract_id、tenant_id、house_id、pay_date应缴日期、amount金额、status0未缴、1已缴、2逾期、create_time。这张表是整个系统财务逻辑的核心。很多人做租赁系统时会把账单逻辑写得很乱要么直接在合同表上加一个“已缴月数”字段要么干脆不生成账单只是在收租时记录一笔流水。但我建议用最传统的方案按月预生成账单。也就是说签约成功后系统根据租期按月循环为每个月生成一条待缴记录。这样后续管理员查看账单列表、统计应收实收、标识逾期状态全部基于数据表不用临时算来算去。生成账单的代码逻辑放在ContractService里合同创建成功后调用generateMonthlyBills(contract)方法用Calendar循环start_date到end_date之间的每个月逐条insert到rent_bill表。这里有个细节当月不足整月的按实际天数折算金额。比如15号搬进来那个月只收半个月租金计算公式是monthly_rent / 当月总天数 * 已住天数。如果不处理这个细节月底一对账就会差出来一堆数字。2.3 为什么报修记录要和合同、房屋都关联报修记录repair字段id、house_id、contract_id、tenant_name、phone、description问题描述、status0待处理、1处理中、2已完成、create_time、finish_time、remark。之所以要同时关联房屋和合同是因为报修既要知道是哪套房子出了问题也要知道当前负责的租客是谁方便联系。设计时要注意contract_id不一定是必填的因为可能房子空置期间也要维修比如水管老化、墙体开裂这时候没有租客只有房屋信息。所以在表单校验时contract_id允许为空但house_id必须填。这种业务细节如果不在设计阶段考虑清楚后面写INSERT语句时就会经常报非空约束错误。另外报修的状态流转建议做成提交报修时状态为“待处理”管理员后台点击“开始处理”变为“处理中”处理完成后点击“完成”同时记录finish_time。不要在数据库里靠改备注文字来标记进度否则统计报表时根本没法聚合数据。3. 从零搭建SSM骨架配置文件的魔鬼细节3.1 pom.xml依赖版本怎么选才不打架SSM项目最头疼的问题之一就是依赖版本冲突。Maven管理依赖虽然方便但如果版本不对经常会出现Spring的jar包版本不一致启动时直接报NoSuchMethodError或者ClassNotFoundException。建议直接固定一套经过验证的组合Spring和SpringMVC用5.1.x版本MyBatis用3.5.xmybatis-spring用2.0.xMySQL驱动用8.0.x对应mysql-connector-java的8.0.x版本Druid连接池用1.2.xJackson用2.9.xPageHelper用5.1.xJSTL用1.2。说几个容易踩的坑。第一如果MySQL是8.0以上版本驱动类名是com.mysql.cj.jdbc.Driver不是老教程里的com.mysql.jdbc.Driver同时URL里必须带serverTimezoneAsia/Shanghai否则日期类型会报错。第二mybatis-spring和MyBatis的版本要匹配我把这两个都固定到对应版本后再没出现过绑定异常。第三JSP页面用JSTL标签时除了引入jstl.jar还要引入standard.jar很多教程只提前者结果c:forEach标签编译报错卡半天。我习惯在pom.xml里把所有依赖的版本用properties统一管理比如写成spring.version5.1.8.RELEASE/spring.version后面只需要改一处就能全局升级其他模块引用时写${spring.version}这样能显著减少版本混乱的问题。3.2 web.xml里的DispatcherServlet和编码过滤器web.xml是SSM项目的入口配置。我部署项目时用的是Servlet 3.1规范web.xml里需要配置两样核心东西DispatcherServlet和CharacterEncodingFilter。DispatcherServlet配置load-on-startup为1意思是Tomcat启动时就初始化SpringMVC容器而不是等到第一个请求过来才初始化。配置文件指向classpath:spring/spring-mvc.xml。同时需要配置url-pattern为/表示所有请求都经过SpringMVC分发包括静态资源的处理需求。CharacterEncodingFilter必须配置在DispatcherServlet之前forceEncoding设置为true这样能保证请求和响应都使用UTF-8编码。这个配置缺失会直接导致一个经典问题前端表单提交的中文到了Controller变成乱码。加了过滤器以后乱码问题消失了但要注意这只解决POST请求的乱码GET请求的乱码还需要改Tomcat的server.xml给Connector添加URIEncodingUTF-8属性。另外别忘了配置404和500的错误页面跳转我在这里用的error-page配置跳转到一个统一的error.jsp页面里展示友好的提示信息避免直接把Tomcat的黄页暴露给用户演示时如果出个堆栈错误页印象分会大打折扣。3.3 spring-mvc.xml的组件扫描和视图解析器spring-mvc.xml是整个Web层的核心配置。我用的配置是开启注解驱动配置组件扫描范围为com.xxx.controller配置静态资源映射配置视图解析器。注解驱动必须要加否则RequestMapping注解不会被识别所有访问都会404。我的写法是 mvc:annotation-driven/ 这个标签还会自动注册JSON消息转换器支持ResponseBody直接返回Java对象转JSON后面给Layui表格返回数据时全靠它。静态资源映射这块很容易被忽略。因为DispatcherServlet的url-pattern配的是/所以JSP、JS、CSS、图片这些静态资源默认也会被拦截。如果不配置mvc:resources mapping/static/** location/static//页面样式会全部失效控制台还会报一堆404。很多人遇到页面“裸奔”的问题十有八九就是这里没配。视图解析器我配的是InternalResourceViewResolverprefix是/WEB-INF/views/suffix是.jsp。这样Controller里只要返回house/listSpringMVC就会自动拼出/WEB-INF/views/house/list.jsp这个路径。把JSP放在WEB-INF目录下还有一个好处浏览器直接访问路径是访问不到的必须通过Controller转发安全性上提升一个档次。3.4 MyBatis配置和jdbc.properties连接参数MyBatis的配置我拆成了两个部分全局配置文件mybatis-config.xml和Spring整合的配置。mybatis-config.xml里主要配置的是驼峰映射开启mapUnderscoreToCamelCase设为true这样数据库的create_time字段能自动映射到Java对象的createTime属性省去一大堆resultMap手写映射。另外配置了日志实现为STDOUT_LOGGING方便在控制台看到SQL执行情况。Spring整合MyBatis时我用的是SqlSessionFactoryBean配置dataSource指向Druid数据源configLocation指向mybatis-config.xmlmapperLocations指向classpath:mapper/*.xml然后配置MapperScannerConfigurer扫描com.xxx.mapper接口包。这样每次新增一个Mapper接口只要包路径对就能自动被Spring管理不需要手动逐个声明。jdbc.properties里写连接信息jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/house_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码Druid连接池配置的核心参数是initialSize5、minIdle5、maxActive20以及maxWait60000。maxWait这个参数很关键如果数据库连接获取不到超过60秒会抛异常而不是无限等下去这样你至少能快速发现连接池耗尽的问题。4. 核心功能实现房源管理、租客签约与租金账单4.1 房源管理增删改查与分页检索的完整链路房源管理是系统最基础的功能模块。我把它拆成四个页面动作列表查询含条件搜索、新增房源、编辑房源、删除房源其中删除是逻辑删除而非物理删除我把house表加了一个del_flag字段值为0正常、1已删除所有列表SQL都带where del_flag0条件。列表查询用的分页是PageHelper插件。Controller里定义PageResult list(Integer page,Integer limit,String houseCode,String status)Service里调用PageHelper.startPage(page,limit)紧跟的Mapper查询会自动变成带LIMIT的SQL查询完成后用PageInfo封装返回。这里有个非常重要的坑要提醒PageHelper.startPage之后必须紧跟第一条SQL查询语句中间不能有任何其他数据库操作。我之前在startPage和Mapper查询之间加了一条日志表的insert操作结果分页参数被日志语句消费掉了房源列表SQL没加成LIMIT直接把全表数据查出来页面卡死。排查了半天才发现是这个原因后面就养成了startPage紧贴目标查询的写代码习惯。新增和编辑房源我放在同一个表单页面。表单里的字段校验前端用Layui的form模块自带的校验规则比如房号必填、租金必须是数字后端在Service里也要再校验一遍因为前端的校验可以被绕过。我试过直接在Controller里写规则后来发现还是放在Service里更合理因为可能多个接口复用同一套校验而且事务控制在Service层校验和数据操作在同一次事务里保证一致性。删除房源时的业务判断是如果房源状态为“已租”不能删除要先处理合同退租才能删。这个规则我在Service里写的是先查contract表有没有status为0的记录有的话直接抛业务异常提示“该房源存在生效中的合同不可删除”然后由全局异常处理器统一把异常信息返回给前端弹窗提示。4.2 签约流程从选房到生成合同的一连串状态联动签约是整个租赁系统的核心业务牵涉到房源状态、租客信息、合同生成、账单生成四个部分。我在页面上设计的是两步操作第一步选择“空闲中”的房源第二步填写租客信息和租期、押金等合同信息。前端房源下拉框的数据来自house表status0的记录这是通过一个独立的接口optionList返回的调用链是HouseController.listOptions() → HouseService.queryAvailableHouses() → Mapper里select id,house_code,address where status0 and del_flag0。提交签约时ContractService.createContract()方法里的逻辑顺序是校验房源状态是否为0防止两人同时提交导致重复签约分布式环境要加锁单机项目可以先update状态再insert合同校验租客身份证是否合法正则验证18位中间有校验位的计算逻辑用网上通用的身份证校验算法就行插入合同记录合同状态为0调用billService.generateMonthlyBills()生成每月租金账单更新房源status为1。这个顺序有一个隐含的事务问题如果步骤3成功但步骤4失败合同有了但账单没生成账就对不上了。解决办法是在createContract()方法上加Transactional注解任何一步抛出RuntimeException整个操作回滚。我测试过故意让账单生成环节抛异常合同记录也自动回滚了两条数据保持一致当时就觉得Spring事务这块没白学。生成账单的时间跨度为contract.startDate到contract.endDate我写了一个循环for (Date month startMonth; month.before(endMonth); month addMonth(month)) { // 计算当月租金首尾月按天折算中间月全额 // insert rent_bill记录status0 }Calendar的日期加减要小心加一个月不是直接currentMonth1而是Calendar.add(Calendar.MONTH,1)它会自动处理跨年的情况比如12月加一个月变成次年1月。如果用错方法跨年合同的账单就会少一个月。4.3 租金管理账单列表、收款确认和逾期标记租金账单列表中我设置的核心检索条件是合同编号、租客姓名、账单状态未缴/已缴/逾期。列表展示字段包括账单编号、租客、房号、应缴月份、应缴金额、状态、操作列。收款确认的业务逻辑很简单点击“确认收款”把rent_bill的status从0改成1记录收款时间pay_time。但要注意如果一笔账单逾期了它应该先标记为2还是可以直接收款我的做法是首页统计时计算出当前时间大于pay_date且status为0的记录自动展示为逾期但不直接更新数据库。这样能避免额外的定时任务查询SQL直接判断状态逻辑效果一样。逾期标记的另一套方案是用定时任务每天凌晨扫一次表把过期的账单更新成逾期状态。我评估了一下对于课设和一般的个人项目定时任务的引入增加了复杂度收获有限不如用SQL动态判断来得简洁。如果你想要的是统计报表部分比如本月应收、实收、逾期金额这些就可以在SQL里用sum(case when ... end)来聚合。一次性把账单明细完成汇总报表直接一条SQL搞定。4.4 报修工单租客提交与管理员处理的后台协同报修模块分成租客端和管理员端两块。租客端的功能是提交报修单填写房屋、问题描述系统自动带出当前房屋的合同信息。管理员端则是报修列表和状态流转操作。这里我用的是同一个Controller、不同的方法区分操作submitRepair()处理租客提交listPending()处理后台待处理查询processRepair()处理开始处理操作finishRepair()处理完成操作。报修列表在Layui表格里展示时状态列展示的是中文状态但数据库存的是0/1/2的数字。我处理这个映射有两种方式第一种是SQL层面case when转换返回中文字段第二种是Java代码里遍历转换。为了保持Layui表格直接渲染、不用前端二次处理我直接在Service层加了一个statusText字段根据status动态赋值这样前端表格直接用templet解析这个字段就行。避免在Controller里对每一个分页的记录做循环转换性能很差不说代码也很丑。我是在Service层查询完之后用stream流的map操作一次性转换简洁且高效。4.5 登录与权限控制拦截器加角色判断的后台安全网租赁系统的后台管理页面不能裸奔必须要登录才能访问。我用SpringMVC的拦截器来实现登录控制。自定义一个LoginInterceptor实现HandlerInterceptor接口在preHandle方法里从Session里取当前登录用户如果没有就重定向到login页面return false如果有就放行。同时在spring-mvc.xml里配置拦截器要拦截的路径是/**所有路径但是excludePathPatterns排除掉/login、/static/**这些不需要登录的资源和请求。除了登录拦截我还加了角色判断。系统用户表sys_user里有个role字段1表示管理员2表示普通操作员。不同角色能访问的菜单不同我在拦截器里除了检查是否登录还检查当前请求URL是否超出角色权限。比如删除合同的操作只有管理员能做普通操作员访问对应的URL会跳转到“无权限”页面。虽然只是一个简单的判断但在答辩演示时体现出的完整度比裸跑一个列表查询系统高很多。权限校验的实现我用了SpringMVC拦截器加上一个自定义的RequiresRole注解在Controller方法上标注拦截器里通过反射读取注解判断扩展性比写死在代码里好很多。但这个方案适合单机小项目如果角色和权限复杂了建议还是引入成熟的权限框架比如Shiro或Spring Security。5. 我踩过的坑和排查手记几个能让你少掉头发的真实问题5.1 MyBatis对象映射失败的排查路径项目跑起来以后最容易出问题的是查询结果封装。数据库字段是create_timeJava对象属性是createTime如果全局配置没开驼峰映射查出来的对象这个字段就是null。开始我以为是自己SQL写错了后来打印日志看到SQL执行正常、返回值也有时间但对象属性是null才意识到是映射开关没开。解决方式是mybatis-config.xml里加一行 或者在每个resultMap里手动显式映射字段。我选择后者配合前者一起用全局开驼峰省事但遇到复杂关联查询时还是手写resultMap更可控因为有些查询会返回多表联查的字段字段名和属性名对不上靠规则映射容易出错。还有一个小细节SQL查询要避免select *尽量把字段名写全。表面上看select *是省事但在多表关联、数据库结构调整后极易出现字段错位。一旦SQL和resultMap映射对不上拿到的是脏数据填到表单里验证半天都不一定发现是字段顺序乱了。写完整字段名的查询虽然SQL长一点但可读性高排查起来也快。5.2 中文乱码的三处源头乱码问题几乎每个做SSM项目的同学都会遇到我把它拆解成三个容易出问题的源头只要按顺序排查就能解决。第一处是数据库连接URL。MySQL连接串必须带useUnicodetruecharacterEncodingutf8这两个参数告诉驱动用Unicode和UTF-8编码来转换传输数据。只写useUnicodetrue效果有限必须加上characterEncoding。第二处是web.xml的CharacterEncodingFilter。这个过滤器要配置在DispatcherServlet之前并且forceEncodingtrue确保请求和响应的编码都被强制为UTF-8。第三处是MySQL数据库本身和表的字符集。建库时用CREATE DATABASE xxx DEFAULT CHARACTER SET utf8mb4utf8mb4是UTF-8的超集可以存emoji等特殊字符避免因为字符集容纳不了而报错。解决完这三处我在表单里提交中文备注也能正常入库、正常显示。这里提一个容易忽略的点MySQL的utf8mb4需要驱动连接字符串也支持MySQL 8.0的驱动默认支持不用额外配置但老驱动比如5.1.x可能要加一个useUnicode参数。5.3 Layui表格数据不显示的坑Layui表格数据不显示是高频问题。它的表格式渲染要求后端返回的JSON必须严格符合格式要求如果缺少count字段或者code不是0表格要么空白要么弹错误提示。我用一个统一的数据结构类Result来处理返回值保证所有接口的输出都是一致的public class Result { private Integer code; // 0表示成功 private String msg; private Integer count; // 总数据量 private List? data; // 当前页的数据 }Controller里所有返回给Layui table的接口都直接返回这个Result对象方法上加ResponseBody即可。对比一开始返回的Map类型或者直接把List返回去这个统一的类避免了每写一个新表格就要调一次返回格式的麻烦也让代码看起来干净很多。在Layui表格的table.render里要把接口URL、page、limit这些参数对应好table.render({ elem: #houseTable, url: /house/list, page: true, cols: [[ {field: id, title: ID}, {field: houseCode, title: 房号}, {field: address, title: 地址}, {field: statusText, title: 状态} ]] });注意Layui会默认传page和limit作为分页参数所以Controller的分页接口参数名要对应接收。我之前接口里写的参数是pageNum和pageSizeLayui传的是page和limit结果前端传了参数后端名字不匹配接收不到排查了半天才发现是参数名不一致。5.4 金额用double还是BigDecimal房屋租金、押金、账单金额这些涉及钱的字段如果用double类型很容易出现精度丢失的问题。比如0.10.2计算出来不是0.3而是0.30000000000000004。Java在浮点数运算时用二进制表示小数有些小数无法精确表示这是很基础但常被忽视的细节。我在项目里把所有金额字段全部使用BigDecimal类型。数据库的字段用decimal(10,2)Java对象用BigDecimal。计算时用BigDecimal的add、subtract、multiply方法除法还要指定精度和舍入方式否则可能出现无限循环小数异常。如果是从前端传来的字符串金额用new BigDecimal(“99.9”)来创建避免用new BigDecimal(99.9)这种先经过double的构造方式后者会得到一堆尾数影响后续所有运算。5.5 租期重叠校验的边界情况前面提到签约时要做租期重叠校验但边界情况没写全这里补一下。重叠不止是“新租期完全覆盖旧租期”还有几种情况新租期开始时间落在旧租期中间、新租期结束时间落在旧租期中间、新租期完全被旧租期包含、新租期的开始时间早于旧租期而结束时间晚于旧租期。这四种情况都是接口里需要拦截的。我写校验SQL时用了一个通用写法select count(*) from contract where house_id #{houseId} and status 0 and start_date #{newEndDate} and end_date #{newStartDate}这个写法很巧妙地覆盖了上面的四个边界只要旧合同的开始时间早于新合同的结束时间同时旧合同的结束时间晚于新合同的开始时间就一定存在交叉点。后来我在别的项目里也用这个套路做时间区间重叠校验验证过很多次相当稳定。5.6 页面缓存导致的调试噩梦在调试过程中前端页面的缓存问题让我吃了不少苦头。改了JSP页面或者JS文件浏览器刷新还是看到旧页面就以为是代码没生效反复重启Tomcat、反复clean项目浪费时间。排查后发现是浏览器缓存。JSP页面在开发阶段可以禁用缓存在JSP页面头部加上%response.setHeader(“Cache-Control”,“no-store”);response.setDateHeader(“Expires”,0);%这样每次请求都会拿最新的页面。对于Layui的JS文件我还用了浏览器开发者工具“Disable cache”选项打开强制不缓存配合IDEA的hot reload功能调试效率明显提升。6. 扩展建议这套系统还能往哪些方向升级6.1 增加租赁到期提醒与自动逾期管理当前系统的到期提醒功能比较基础只能靠人工查看合同列表有没有临近过期的。合理扩展方向是在合同表增加一个remind_flag字段每天定时任务扫描租期结束时间在30天内且提醒状态为0的合同生成提醒记录或推送站内信。账单逾期同理可以用同样的定时任务框架把status为0且应付日期早于当前日期的账单统一标记为逾期。6.2 引入短信或邮件通知如果想让系统更接近真实产品可以接入短信服务或者邮件服务。例如在账单生成后系统自动向租客手机号发送一条“本月租金应缴”的短信。再比如合同快到期时发提醒短信。不需要自己搭服务器用国内常见的云服务商提供的短信接口就行。对于课设项目这个功能展示出来相当加分。6.3 增加图形化统计报表目前的收租统计只是一个简单的列表汇总只能看表格没法看趋势。升级方向是引入图表库渲染可视化的月租趋势图、房源利用率饼图、逾期金额柱状图。前后端分离的方案下可以用ECharts配合AJAX拉数据SSM里也可以在JSP页面直接引入ECharts的CDN文件然后Controller返回统计数据JSON。核心还是把数据查出来按照图表库要求的格式组装好。6.4 升级为SpringBoot版本SSM这套系统稳定跑通之后如果时间充裕可以尝试改造成SpringBoot版本。核心改动在于把web.xml、spring-mvc.xml、mybatis-config.xml这些XML配置迁移到注解和application.yml依赖管理统一用SpringBoot的starter。这个过程是很好的学习机会因为你对SSM的运转逻辑已经门儿清迁移时遇到问题能快速定位学到的直接是SpringBoot自动配置和约定优于配置的思路。以我个人经验来说做这种SSM管理系统项目最大的收获不是会用框架而是通过业务需求学会了如何设计分层、设计表、处理事务和排查线上问题。它不像做一个炫酷的算法或App界面那么吸引眼球但真实业务系统里的琐碎和严谨都在这些代码里体现得明明白白。如果你正在做类似的系统把上面这些细节都过一遍答辩时被问到底层原理或者业务边界条件都能答得比多数同学更扎实。