ARTICLE DETAIL

资讯详情

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

基于SpringBoot+JavaWeb的房屋租赁系统开发全流程解析

基于SpringBoot+JavaWeb的房屋租赁系统开发全流程解析 我前阵子帮一个学弟的毕设做技术指导题目就是这套“基于SpringBoot的JavaWeb大学生房屋租赁系统”。本来以为又是一套普普通通的CRUD管理系统结果真做下来发现里面值得讲的东西还挺多——特别是对于一个没有太多真实项目经验的同学来说从需求分析、表设计到接口联调、服务器部署整套流程走一遍对JavaWeb和SpringBoot的理解绝对能上一个台阶。这篇文章我按自己做项目时实际推进的顺序来写怎么拆需求、怎么定技术方案、数据库表怎么设计、核心功能怎么实现、部署的时候又会踩哪些坑。全程用我实际敲过的代码、写过的SQL和调过的Bug来还原适合准备做类似毕设、或者想拿SpringBoot练手写一个完整项目的同学参考。1. 项目整体设计与技术选型1.1 系统定位与核心需求拆解先把这个系统的定位说清楚。所谓“大学生房屋租赁系统”本质是一个面向校园周边租房场景的信息撮合平台。它跟市面上那些大型中介平台贝壳、自如之类最大的区别在于目标用户是大学生场景相对封闭房源主要集中在学校周边需求主要集中在“找房—看房—签约—入住”这条链路上。我接手这个题目之后第一件事不是打开IDE写代码而是先把用户角色和核心业务流程捋清楚。这套系统里光是有明确诉求的角色就有三类租客学生需要能注册登录、浏览房源、按条件搜索筛选价格、户型、朝向、楼层、收藏心仪房源、提交预约看房申请、下单租房、查看自己的订单状态。房东或转租的个人需要能发布房源、管理自己发布的房源上下架、查看租客发来的预约和订单、确认订单并推动交易进行。管理员需要能审核房源信息避免虚假房源和违规内容、管理注册用户禁用违规账号、查看全平台订单数据、发布站内公告。把这几类需求做成一张功能脑图你就能发现这其实覆盖了JavaWeb课程里几乎所有的经典场景用户认证登录注册、多条件检索SQL拼接/动态查询、状态流转订单状态从待确认到已签约、权限控制不同角色看到不同菜单、文件上传房源图片、后台管理审核与统计。很多同学拿到题目就急着写代码这是大忌。我建议你先花一两天时间画用例图、画业务流程图把“谁在什么场景下要做什么事”想明白后面写代码会快很多。这也是论文里第一章第二章的核心素材。1.2 技术栈选择为什么是SpringBoot JavaWeb题目里直接写了SpringBoot和JavaWeb这不是随便定的。我猜测要么是学校课程大纲要求的要么是导师指定的方向。实际做的时候这个组合的合理性可以从两个层面理解。第一JavaWeb是底子。Servlet、Filter、Listener、Session、Cookie、JSP这些是JavaWeb开发的根基。你可能在实际项目里很少直接用Servlet写Handle但你用SpringBoot时写的Controller、用的拦截器、操作Session本质上都是在Servlet规范之上做封装。所谓的“JavaWeb项目”就是要求你理解HTTP请求从浏览器出发经过服务器、Servlet容器、Filter链最终到达业务代码的完整过程。第二SpringBoot是效率工具。它的价值在于解决了传统SSM/SSH项目里繁琐的XML配置问题。SpringBoot用自动装配的方式替代了大量手动配置——比如内嵌Tomcat让你不用再单独装服务器、starter机制帮你把依赖管理做好、application.properties/yml集中管理配置项。对一个毕设项目来说SpringBoot能让开发重心从“配环境”回到“写业务”这非常关键。我在实际做的时候技术栈选了这套层级技术选型说明后端框架Spring Boot 2.7.x稳定版本兼容性好网上资料最多持久层MyBatis-Plus单表CRUD不用写SQL复杂查询手写XML数据库MySQL 8.0用得最广Navicat可视化管理前端渲染Thymeleaf Bootstrap服务端渲染适合毕设演示不用拆前后端权限认证Session 拦截器简单可靠比JWT更适合这个规模的项目构建工具Maven标准JavaWeb项目构建方式开发工具IntelliJ IDEA一定要用IDEA别用Eclipse了这里有个判断要提醒大家不要为了炫技把技术栈堆得太高。我见过有同学用Spring Cloud Alibaba微服务架构去做一个宿舍管理系统结果服务注册发现、网关路由、分布式事务这些概念还没搞明白就把自己绕晕了。毕设评分看重的是“业务完整度技术匹配度工作量”不是技术名词的数量。SSM/SpringBoot MySQL认认真真做完把细节打磨好得分一定不会低。1.3 工程结构与代码分层设计项目结构我严格按照经典的三层架构来组织这也是JavaWeb项目最规范的做法。Module结构大致是这样的com.example.houserent ├── controller # 控制层接收请求、参数校验、返回页面或JSON │ ├── UserController │ ├── HouseController │ ├── OrderController │ └── AdminController ├── service # 业务层核心业务逻辑、事务控制 │ ├── UserService │ ├── HouseService │ └── OrderService ├── mapper # 持久层MyBatis-Plus的Mapper接口 │ ├── UserMapper │ ├── HouseMapper │ └── OrderMapper ├── entity # 实体类对应数据库表 │ ├── User │ ├── House │ └── Order ├── dto # 数据传输对象Vo类用于接口返回 ├── interceptor # 拦截器登录验证、管理员权限验证 ├── config # 配置类WebMvcConfig、MybatisPlusConfig └── common # 公共类统一返回结果、异常处理、工具类这种分层的好处是职责单一。Controller只负责接收参数和返回结果业务逻辑全在Service里数据库操作全在Mapper里。后面系统出问题了排查起来会很轻松——哪一层出错就定位哪一层不至于所有代码堆在Servlet里乱成一团。2. 核心功能模块与数据库设计2.1 功能模块拆解三个角色六块业务整个系统我拆成了六大功能模块覆盖三种角色的核心操作。实际做的时候优先把主链路做完——也就是从“租客搜索房源”到“提交订单”这一条完整链路先把骨架打通再往上面加副功能。模块一用户认证模块注册、登录、退出登录、修改密码、个人信息维护。用户表里用role字段区分身份租客/房东/管理员。注册时必须做参数校验非空、邮箱格式、手机号格式、用户名唯一性密码不能明文存储至少要用MD5加盐或BCrypt加密。模块二房源展示模块房源列表分页展示、房源详情页、按区域/价格/户型/朝向进行多条件筛选、关键词模糊搜索。这里要处理图片一张房源通常有多张图片用一对多的表结构存图片路径页面上用轮播图展示。模块三房源管理模块房东登录后可以发布新房源维护自己已发布的房源编辑、上下架。发布表单里的字段很多——房屋标题、描述、租金、押金、面积、户型、朝向、楼层、所在小区、地址、配套设施床、衣柜、空调、热水器、WiFi等、图片上传。审核状态有三个待审核、已通过、已驳回。模块四预约看房模块这是很多类似系统容易忽略但是实际上很有用的功能。租客在房源详情页可以发起“预约看房”选择期望看房时间填写联系电话。房东在后台能看到收到的预约申请确认或拒绝。这个功能还原了真实的租房流程——先看房再签约不是线上看图片就拍板。模块五租赁订单模块租客下单租房、房东处理订单订单状态是一个状态机待确认 → 已确认 → 已签约 → 已完成中间还有一个可选的“已取消”。每个状态转移都要校验权限——租客只能取消自己的订单房东才能确认自己房源的订单。这模块是事务控制最密集的地方涉及库存房源可租状态和订单状态的联动。模块六后台管理模块管理员登录独立的后台管理界面功能包括用户列表可禁用账号、房源列表审核通过/驳回、订单列表查看全平台交易、公告管理发布/删除公告。管理员账号我直接通过SQL脚本插入数据库不提供注册入口保证安全性。2.2 数据库表设计从E-R图到建表语句数据库设计是这套系统最见功力的地方。表设计得好不好直接决定后面写SQL的复杂度和系统的扩展性。我最终设计了六张核心表外加两张辅助表。这里把最关键的四张表拿出来讲。用户表 t_user字段名类型说明idbigint主键自增usernamevarchar(50)用户名唯一索引passwordvarchar(100)加密后的密码nicknamevarchar(50)昵称real_namevarchar(50)真实姓名phonevarchar(20)手机号用于看房联系emailvarchar(100)邮箱roletinyint角色0-租客1-房东2-管理员statustinyint状态0-正常1-禁用avatarvarchar(255)头像路径create_timedatetime注册时间房源表 t_house最复杂的表字段比较多字段名类型说明idbigint主键landlord_idbigint发布人ID外键关联t_usertitlevarchar(100)房源标题descriptiontext房源描述communityvarchar(100)小区/公寓名称addressvarchar(255)详细地址pricedecimal(10,2)月租金元depositdecimal(10,2)押金areadecimal(5,1)面积平方米bedroomtinyint户型-室living_roomtinyint户型-厅bathroomtinyint户型-卫orientationvarchar(10)朝向南/北/东西等floorvarchar(20)楼层rent_typetinyint租赁方式0-整租1-合租facilitiesvarchar(255)配套设施JSON数组或逗号分隔cover_imagevarchar(255)封面图路径statustinyint房源状态0-待审核1-已上架2-已下架3-已出租audit_remarkvarchar(255)审核驳回原因view_countbigint浏览次数create_timedatetime发布时间订单表 t_order字段名类型说明idbigint主键order_novarchar(32)订单编号唯一格式当前时间戳随机数house_idbigint房源IDtenant_idbigint租客IDlandlord_idbigint房东ID冗余存储方便查询rent_pricedecimal(10,2)成交月租金下单时的价格快照depositdecimal(10,2)押金lease_termsvarchar(50)租期如12个月statustinyint订单状态0-待确认1-已确认2-已签约3-已完成4-已取消create_timedatetime下单时间update_timedatetime最近更新时间预约看房表 t_appointment字段名类型说明idbigint主键house_idbigint房源IDtenant_idbigint预约人IDphonevarchar(20)联系电话appoint_timedatetime期望看房时间remarkvarchar(255)备注statustinyint状态0-待房东确认1-已确认2-已拒绝create_timedatetime提交时间还有收藏表 t_favorite记录用户收藏的房源和公告表 t_notice这两个比较简单就不展开列字段了。表设计上有一个经验要记住订单表里一定要存价格快照也就是下单那一刻的租金单价和总金额。不能为了省事去join房源表实时取价——万一房东改价了订单历史就该跟着变这在业务上是不对的。把成交时的关键信息冗余到订单表里保证订单数据的不可变。另外一个细节是表字段类型的选择。金额字段一定用decimal浮点型double在涉及钱的时候会有精度问题时间字段建议统一用datetime别混合用timestamp虽然timestamp有时区处理但1970年之前存不了处理起来更麻烦逻辑删不用真的delete用一个delete_flag字段标记免得以后想恢复数据没得恢复。3. 实操过程从0到1搭建开发环境与实现核心代码3.1 IDEA创建SpringBoot项目与基础配置项目落地第一步是环境搭建。IDEA 2022以上版本用自带的Spring Initializr创建项目非常方便New Project → Spring Initializr → 选择Java版本用JDK 1.8就够了实际用8u202版本最稳妥 → 填Group和Artifact → 勾选依赖即可。我这里要特别提醒一下热门问题SpringBoot版本别选太高的。有些同学喜欢在Initializr里直接选最新的3.x版本然后用JDK 17甚至21去开发。看起来没什么问题但实际上很多网上教程、开源代码、MyBatis-Plus的版本兼容性都是基于SpringBoot 2.x写的用3.x会遇到启动报错、依赖不兼容等一堆问题。实测下来SpringBoot 2.7.18这个版本最省心——支持JDK8、兼容老教程、MyBatis-Plus完全适配对于毕设项目来说完全够用。创建完项目的pom.xml核心依赖我列出来供参考parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web模块含内嵌Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis-Plus注意版本适配 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- Lombok省掉getter/setter -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency /dependencies然后就是application.yml配置文件的编写这是整个项目启动的地基server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/house_rent?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的数据库密码 thymeleaf: cache: false # 开发时关闭模板缓存改页面不用重启 mybatis-plus: configuration: map-underscore-to-camel-case: true # 驼峰映射数据库字段下划线转实体类驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期打印SQL日志 global-config: db-config: logic-delete-field: deleted # 逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0这个配置文件里有几个细节值得展开说明。第一URL里的serverTimezone参数必须加。如果不指定时区MySQL 8.0连接时报“Could not create connection to database server”的错误原因是默认时区America/Los_Angeles和中国时区差了15个小时。加上serverTimezoneAsia/Shanghai后这个问题就消失了。第二map-underscore-to-camel-case这个配置太重要了。Java实体类习惯用驼峰命名比如createTime数据库字段习惯用下划线create_time这个配置能让MyBatis自动完成映射。忘了配的后果是查出来的对象全是null光排这个错就能浪费半天。第三开发环境一定要开启SQL日志。看到控制台每一条SQL的执行情况排查问题时都是救命级别的帮助。3.2 核心业务代码实现登录认证与多条件房源检索基础配置搞定后我按照模块优先级逐个实现核心功能。这里挑两个最有含金量的部分展开讲登录认证和房源多条件检索。登录认证实现登录认证用Session 拦截器方案代码简单但功能完整。比用JWT更适合这个项目——因为服务端渲染的页面不需要无状态APISession天然契合。登录流程如下PostMapping(/user/login) public String login(UserLoginDto dto, HttpSession session, Model model) { // 1. 参数校验用户名和密码不能为空 if (StringUtils.isEmpty(dto.getUsername()) || StringUtils.isEmpty(dto.getPassword())) { model.addAttribute(error, 用户名和密码不能为空); return login; } // 2. 调用业务层做认证 User user userService.login(dto.getUsername(), dto.getPassword()); // 3. 认证失败返回错误信息 if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } // 4. 检查账号是否被禁用 if (user.getStatus() 1) { model.addAttribute(error, 账号已被禁用请联系管理员); return login; } // 5. 保存登录状态到Session session.setAttribute(loginUser, user); // 6. 按角色跳转到不同首页 if (user.getRole() 2) { return redirect:/admin/index; } return redirect:/house/list; }然后是拦截器用来保护需要登录才能访问的资源public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断Session中是否有登录用户 HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user ! null) { return true; } // 未登录跳转登录页 response.sendRedirect(request.getContextPath() /user/login); return false; } }在WebMvcConfig中注册拦截器并指定要拦截和排除的路径Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns( /, /user/login, /user/register, /house/list, /house/detail/**, /css/**, /js/**, /images/**, /error ); }密码加密我用的是BCryptSpring Security的BCryptPasswordEncoder类单独引依赖或者用Spring自带的工具都行。这一点要特别强调明文密码存储是安全红线。哪怕这是课程设计我也希望你把加密这步做好——这是作为一个开发者的基本职业素养。房源多条件检索实现房源列表页需要支持多个筛选条件关键词模糊匹配标题和描述、租金范围最低价、最高价、户型几室、朝向、租赁方式、排序方式最新、价格升序、价格降序、浏览次数。这里用MyBatis-Plus的Wrapper做单表动态查询会非常顺手public PageHouse queryHouseList(HouseQueryDto query) { // 分页插件初始化 PageHouse page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperHouse wrapper new LambdaQueryWrapper(); // 关键词搜索标题或描述模糊匹配 if (StringUtils.isNotBlank(query.getKeyword())) { wrapper.and(w - w .like(House::getTitle, query.getKeyword()) .or() .like(House::getDescription, query.getKeyword())); } // 租金范围筛选 if (query.getMinPrice() ! null) { wrapper.ge(House::getPrice, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(House::getPrice, query.getMaxPrice()); } // 户型筛选室 if (query.getBedroom() ! null) { wrapper.eq(House::getBedroom, query.getBedroom()); } // 朝向筛选 if (StringUtils.isNotBlank(query.getOrientation())) { wrapper.eq(House::getOrientation, query.getOrientation()); } // 租赁方式筛选 if (query.getRentType() ! null) { wrapper.eq(House::getRentType, query.getRentType()); } // 核心条件只查已上架 wrapper.eq(House::getStatus, 1); // 排序 if (price_asc.equals(query.getSort())) { wrapper.orderByAsc(House::getPrice); } else if (price_desc.equals(query.getSort())) { wrapper.orderByDesc(House::getPrice); } else if (view_desc.equals(query.getSort())) { wrapper.orderByDesc(House::getViewCount); } else { wrapper.orderByDesc(House::getCreateTime); } return houseMapper.selectPage(page, wrapper); }这里的写法体现了LambdaQueryWrapper的优势——不需要写任何SQL字符串靠方法引用保证字段名的编译期检查不会出现手写SQL时“字段名拼写错误但运行到那一行才报错”的窘境。而且分页是MyBatis-Plus自动帮你加LIMIT实现的省了PageHelper可能出现的插件版本冲突问题。3.3 数据库初始化脚本建库建表脚本是我一次性写完的挂在项目根目录的sql文件夹里。核心表建表语句示例CREATE DATABASE IF NOT EXISTS house_rent DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE house_rent; -- 用户表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码, nickname VARCHAR(50) COMMENT 昵称, real_name VARCHAR(50) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 手机号, email VARCHAR(100) COMMENT 邮箱, role TINYINT NOT NULL DEFAULT 0 COMMENT 角色 0-租客 1-房东 2-管理员, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0-正常 1-禁用, avatar VARCHAR(255) COMMENT 头像, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 房源表 CREATE TABLE t_house ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 房源ID, landlord_id BIGINT NOT NULL COMMENT 房东ID, title VARCHAR(100) NOT NULL COMMENT 标题, description TEXT COMMENT 房源描述, community VARCHAR(100) COMMENT 小区, address VARCHAR(255) COMMENT 地址, price DECIMAL(10,2) NOT NULL COMMENT 月租金, deposit DECIMAL(10,2) NOT NULL COMMENT 押金, area DECIMAL(5,1) COMMENT 面积, bedroom TINYINT COMMENT 室, living_room TINYINT COMMENT 厅, bathroom TINYINT COMMENT 卫, orientation VARCHAR(10) COMMENT 朝向, floor VARCHAR(20) COMMENT 楼层, rent_type TINYINT DEFAULT 0 COMMENT 租赁方式 0-整租 1-合租, facilities VARCHAR(255) COMMENT 设施, cover_image VARCHAR(255) COMMENT 封面图, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0-待审核 1-已上架 2-已下架 3-已出租, audit_remark VARCHAR(255) COMMENT 审核备注, view_count BIGINT DEFAULT 0 COMMENT 浏览量, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间, INDEX idx_landlord (landlord_id), INDEX idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表;这里我用了utf8mb4而不是utf8——这是一个蚂蚁虽小但很重要的坑。utf8在MySQL里实际是utf8mb3只能存3字节的字符像emoji表情这些4字节字符会插入报错。utf8mb4是完整的UTF-8编码能存所有Unicode字符而且向下兼容utf8mb3所以新项目一律用utf8mb4。另外一个细节是在需要做查询条件的字段上建索引。比如status经常按状态筛选、landlord_id经常按房东查房源、create_time经常按时间排序。加了索引后数据量即使到了几万条查询速度也能保持在毫秒级。3.4 前端页面与Thymeleaf模板渲染这套系统我选择用Thymeleaf做服务端渲染而不是Vue前后端分离。这个选择不是拍脑袋装的——主要是考虑到毕设项目的答辩场景评委老师打开地址就能看到完整页面不用再教他启动前端Node服务。虽然Vue前后端分离是现在企业的主流模式但Thymeleaf把动态数据直接渲染到HTML里代码量少、调试方便、部署简单对单体系统来说其实非常合理。页面组织结构如下src/main/resources/templates/ ├── login.html # 登录页 ├── register.html # 注册页 ├── index.html # 首页重定向 ├── house/ │ ├── list.html # 房源列表页核心页面搜索分页 │ ├── detail.html # 房源详情页轮播图发起预约/下单 │ └── publish.html # 发布房源页表单图片上传 ├── user/ │ ├── center.html # 个人中心我的信息 │ ├── myOrders.html # 我的订单租客视角 │ └── myHouses.html # 我的房源房东视角 └── admin/ ├── index.html # 管理后台首页 ├── userList.html # 用户管理 ├── houseAudit.html # 房源审核 └── orderList.html # 订单管理Thymeleaf的关键语法其实就三四个th:each循环、th:if条件判断、th:text输出文本、th:href拼接URL。比如房源列表页面div classhouse-card th:eachhouse : ${pageRecords} a th:href{/house/detail/{id}(id${house.id})} img classhouse-img th:src${house.coverImage} alt封面图 /a div classhouse-info h3 classhouse-title th:text${house.title}标题/h3 p classhouse-price span classprice-num th:text${#numbers.formatDecimal(house.price, 1, 2)}2500.00/span元/月 /p p classhouse-desc th:text${house.bedroom}室${house.livingRoom}厅${house.bathroom}卫 · ${house.area} ㎡ 户型描述 /p p classhouse-address th:text${house.community}小区名称/p /div /div分页组件我用了一个简单的Java工具类封装前台配合一个公共的翻页DIV上一页、页码数、下一页关键是要在URL里带上当前已选择的查询条件否则翻了页筛选条件就丢了。这个问题在我带的学生里出现率极高——很多人翻页后条件丢失排查了半天最后发现是URL拼接少了参数。3.5 图片上传与静态资源映射房源发布需要传实拍图。市面上的成熟方案是传对象存储MiniO或者云OSS但考虑到这是毕设项目最终选择存本地目录——UploadFileUtil把MultipartFile写到项目下的upload目录然后通过WebMvc配置映射成可访问的URL。上传代码核心逻辑如下public String uploadImage(MultipartFile file, HttpServletRequest request) { // 1. 空文件校验 if (file.isEmpty()) { throw new BizException(上传文件不能为空); } // 2. 校验文件类型 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); ListString allowTypes Arrays.asList(.jpg, .jpeg, .png, .gif); if (!allowTypes.contains(ext.toLowerCase())) { throw new BizException(仅支持jpg/png/gif格式图片); } // 3. 生成不重复的文件名时间戳UUID String newFileName System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ) ext; // 4. 构建存储目录按日期分目录存放 String dateDir new SimpleDateFormat(yyyy/MM/dd).format(new Date()); String realPath System.getProperty(user.dir) /upload/ dateDir /; File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } // 5. 保存文件 file.transferTo(new File(realPath newFileName)); // 6. 返回可访问的相对路径 return /upload/ dateDir / newFileName; }注意这里有个容易踩的坑transferTo方法在Windows和Linux上的行为不一样。Windows上如果目标文件已存在可能会报错所以文件名里加时间戳和UUID是为了彻底避免重名。另一个坑是上传文件的大小限制——SpringBoot默认最大1MB超了就报MaxUploadSizeExceededException需要在application.yml里调大spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB静态资源映射配置方式Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将/upload/**映射到本地磁盘目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); } }这里要注意的是路径拼接格式addResourceLocations的file:后面必须以/结尾不然资源映射会404。这个问题我排查过很久最后才意识到是因为末尾少了一个斜杠。4. 踩坑实录遇到了哪些问题又是怎么解决的4.1 环境配置类IDEA运行JavaWeb项目配置与Maven构建第一个让不少同学卡住的问题是怎么在IDEA里正确配置一个JavaWeb项目的运行环境。对于SpringBoot项目其实简单——main方法直接跑内嵌Tomcat会自动启动。但是对于传统JavaWeb项目外置Tomcat部署war包就需要配置Tomcat的运行时环境了。我实际开发时用SpringBoot为主就没有额外配外部Tomcat。但如果你的毕设要求必须打成war包部署到外置Tomcat有几步必须注意要让SpringBoot的启动类继承SpringBootServletInitializer打包方式要改成war而不是jar部署的时候把war包改名成ROOT.war放到Tomcat的webapps目录下否则访问路径里会带个项目名看起来不够干净。Maven这块也有个高频问题IDEA新建SpringBoot项目时Maven下载依赖极慢甚至卡住。主要原因是默认从Maven中央仓库拉包国内的网络环境下载速度很慢。解决办法是把Maven仓库地址换成国内镜像在settings.xml里配置阿里云镜像。另外一个极其常见的问题是Maven依赖冲突。比如项目中同时引入不同版本的某个库启动时会报类似“NoSuchMethodError”的错误。排查方式是执行mvn dependency:tree查看依赖树找到重复依赖后用exclusion标签排除掉。4.2 业务逻辑类事务失效、时区偏差与分页插件开发过程中有一个业务问题让我印象很深房东确认订单的时候需要同时更新订单状态和房源状态两件事必须在一个事务里完成。一开始我只在Service方法上加了Transactional注解结果测试发现只更新了订单状态而房源状态没变。排查到最后发现是同一个类内部方法调用导致事务失效——Spring的声明式事务基于AOP代理同类方法之间的调用不走代理所以注解不生效。解决办法有两种一种是把内部方法单独拆到一个Service类里通过注入代理对象调用另一种是自己注入AopContext.currentProxy()来调用。我更推荐第一种类职责清晰规避了事务失效的坑。时区偏差的问题也很典型。测试项目的时候发现发布时间比当前时间整整早了8个小时。原因其实在上面配置里已经埋了伏笔——JDBC连接URL里已经加了serverTimezoneAsia/Shanghai但是MySQL数据库本身的时区没设对导致数据库里存的是东八区时间查询的时候JDBC又按默认时区解析了一遍一来一回就差了8小时。彻底解决办法是MySQL连接参数加connectionTimeZoneAsia/Shanghai并在建库SQL里指定DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci同时把MySQL服务器时区也改为08:00三管齐下。MyBatis-Plus分页失效也是老生常谈的问题。很多同学直接在Mapper里写了分页查询结果发现查出来的数据是全量而不是一页几条原因是没有配置MyBatis-Plus的分页插件。需要用Configuration类注册一个MybatisPlusInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }加了这段代码之后selectPage方法才会自动拼接LIMIT语句否则只是内存分页数据量大时性能崩。4.3 部署运维类打jar包部署与常见启动异常最后部署到服务器上演示我选择打成jar包直接运行。打包之前要先把测试类排除掉并确认数据库连接配置正确然后执行mvn clean package -DskipTests。打包成功后在target目录下会生成一个可执行的jar丢到服务器上用命令启动nohup java -jar house-rent.jar --spring.profiles.activeprod app.log 21 nohup和配合可以保证退出SSH会话后程序继续运行。日志重定向到app.log方便查看运行状态。如果服务器端口被占用可以这样查netstat -anp | grep 8080 kill -9 进程ID或者直接在启动时指定别的端口java -jar house-rent.jar --server.port9090部署时还有一个很隐蔽的问题之前开发时上传的房源图片地址存的是相对路径比如/upload/2024/xx.jpg但打成jar包后user.dir的值变成了jar包所在的目录而不再是项目根目录。所以如果上传目录配置不对可能导致图片无法访问。我的解决方式是在application.yml里配置一个file.upload-path变量部署时通过启动参数覆盖它的值确保upload目录指向服务器上一个固定的绝对路径。5. 写在最后这套系统的价值不止于项目本身这段时间做下来我的体会是技术本身并不难难的是把一个模糊的需求真正落地成一个能用、经得起推敲的系统。这套房屋租赁系统虽然是个典型的毕设项目管理类项目但它背后涉及的每一个点——表设计、动态查询、事务控制、权限拦截、文件上传、部署发布——都是实际开发中的核心能力。如果你正准备做类似题目我的建议是不要急着一上来就敲代码先花一周时间把需求、表和原型理清楚再动手。写代码的时候也千万不要只顾着调通就完事多想一想“为什么”和“如果数据量大了我还能不能扛得住”。用这套思路做完一个完整的项目你收获的绝不只是一个答辩能过的东西而是一套解决问题的方法论。最后分享一个小技巧每实现一个模块就写一段测试数据并截图存档。不要等全部做完了再补截图那时候你可能已经没有精力去重新造数据了。论文里的运行截图、演示用的测试账号、都能从这些日常积累里拿出来到最后你会感谢自己当初这个习惯。希望这篇内容对你有用。如果你在照着做的时候碰到了具体的报错或者卡点别慌——先看日志、查配置、搜关键字大部分问题你踩过的坑别人也一定踩过。项目是一个一个坑踩出来的也是在这个过程里真正学到东西的。
返回列表