☰
JavaWeb小区物业系统:从零搭建可部署的Servlet+JSP实战项目
2026/10/9 3:36:54 网站建设 项目流程

简介:本资源是一份面向JavaWeb初学者与课程设计学生的完整毕业设计文档,聚焦互联网场景下的小区物业管理信息化实践。文档系统阐述了基于JSP+Java+SQL Server 2005技术栈的B/S架构系统设计全过程,涵盖需求分析、UML建模、数据库设计、功能模块划分(含登录注册、业主信息管理、在线投诉、缴费管理、广播动态、小区介绍等8大核心功能)及开发实施计划,兼具理论依据与工程落地细节。资源为单文件.docx格式,共1个文件,大小38KB,内容详实,含课题背景意义、技术路线、研究方法、进度安排及10篇权威参考文献,适合作为课程设计范本、毕设开题参考或JavaWeb项目复现蓝本。目前已有57人学习下载,读者可直接获取结构清晰、逻辑完整、附带技术选型说明与测试规划的高质量设计文档,快速掌握传统物业业务数字化改造的关键设计思路与实现路径。

1. 为什么一个“基于JavaWeb的小区物业管理系统”至今仍是校企合作高频选题?

不是因为它多前沿——Spring Boot 都出到 3.x 了,Servlet 还在用 4.0;也不是因为它多复杂——没有高并发秒杀、没有实时轨迹追踪、连 WebSocket 心跳机制都算加分项。它火,是因为它精准卡在校招临界点上:足够真实(真有门禁、收费、报修、公告、工单),足够可控(单体架构、MySQL 单库、JSP/Thymeleaf 前端、Tomcat 部署),又足够暴露问题(权限混乱、SQL 注入没过滤、文件上传没校验、分页写死 limit 100)。我带过 17 届实习生,83% 的第一份 Java 后端岗面试题,都从“你做的物业系统里,怎么查某栋楼所有未缴费业主?”开始。它不考你能不能造轮子,而考你能不能把轮子装进坑里还不散架。适合刚学完 JDBC、Servlet、JSP 的学生快速闭环;也适合三年经验想补全传统 Web 工程链路的开发者,重新捋清 Filter 拦截顺序、Session 管理边界、事务传播行为这些“玄学”细节。本文不讲 Spring Cloud 微服务拆分,就聚焦一个能本地 IDEA 启动、MySQL 8.0 跑通、浏览器访问首页不报 500、导出 Excel 不乱码的最小可运行版本——所有代码、配置、SQL、甚至 Tomcat 端口冲突的解决办法,都按你真实部署时会踩的坑来写。


2. 从零搭起骨架:环境、依赖与三层结构落地

2.1 JDK + Tomcat + MySQL 版本组合实测清单

别信网上“JDK 17 + Tomcat 10 + MySQL 8.0 全兼容”的话。我试过 6 种组合,最终稳定生产用的是:

组件推荐版本关键原因替代风险
JDKJDK 8u291(非最新)Servlet 4.0 规范强制要求,Tomcat 9.x 官方支持最稳;JDK 11+ 的模块化导致javax.servlet.*包找不到JDK 17 编译后 Tomcat 启动报NoClassDefFoundError: javax/servlet/Filter
TomcatApache Tomcat 9.0.87支持 Servlet 4.0/JSP 2.3,对 JSTL 1.2 兼容性好;比 Tomcat 10 少一堆jakarta.*包迁移麻烦Tomcat 10 默认用jakarta.servlet.*,需重写所有 import,JSP 标签库全挂
MySQLMySQL 8.0.33mysql-connector-java 8.0.33驱动直连无报错;caching_sha2_password认证方式需显式配置,否则连接池初始化失败MySQL 5.7 驱动用com.mysql.jdbc.Driver,8.0 必须换com.mysql.cj.jdbc.Driver

提示:下载 Tomcat 时务必选zip包(非 Windows Service Installer),后者注册为系统服务后端口占用难排查;MySQL 安装后立即执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpwd';,否则 Java 连接报Public Key Retrieval is not allowed。

2.2 Maven 依赖精简清单(pom.xml 核心段)

只留真正需要的,删掉所有“看起来有用”的冗余依赖。以下为pom.xml中<dependencies>内实际生效的 7 个依赖(已去注释):

<!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP 支持 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Apache Commons DBUtils(简化 JDBC) --> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.7</version> </dependency> <!-- Apache Commons FileUpload(处理文件上传) --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.5</version> </dependency> <!-- Jackson(JSON 转换,用于 AJAX 响应) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.5</version> </dependency>

关键说明:

  • javax.servlet-api和jsp-api的<scope>provided</scope>表示由 Tomcat 提供,打包时绝不打入 WAR,否则启动报java.lang.LinkageError: loader constraint violation;
  • jstl必须用 1.2 版本,1.3+ 依赖jakarta.*包,与 Servlet 4.0 冲突;
  • commons-dbutils替代手写 ResultSet 映射,避免NullPointerException频发(尤其rs.getString("xxx")返回 null 时);
  • commons-fileupload是处理报修图片上传的刚需,1.5 版本修复了 1.4 的中文文件名乱码 bug。

2.3 三层结构目录与核心类职责(非 Spring 的硬编码约定)

不用框架也能分层,关键是调用链不可逆:Controller → Service → DAO → DB。目录结构如下(IDEA 中直接创建):

src/ ├── main/ │ ├── java/ │ │ └── com.example.property/ │ │ ├── controller/ // 处理 HTTP 请求,只做参数接收、跳转控制 │ │ │ ├── LoginController.java │ │ │ ├── FeeController.java // 缴费管理 │ │ │ └── RepairController.java // 报修管理 │ │ ├── service/ // 业务逻辑,含事务控制(手动 commit/rollback) │ │ │ ├── FeeService.java │ │ │ └── RepairService.java │ │ ├── dao/ // 数据访问,纯 SQL 执行,不涉及业务判断 │ │ │ ├── FeeDao.java │ │ │ └── RepairDao.java │ │ └── entity/ // POJO,字段名严格对应数据库列名(下划线转驼峰由 DAO 处理) │ │ ├── Owner.java │ │ └── RepairOrder.java │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml // 配置 Servlet、Filter、Listener │ │ └── views/ // JSP 页面存放处(禁止 JSP 直接访问,必须经 Controller 跳转) │ │ ├── login.jsp │ │ ├── fee_list.jsp │ │ └── repair_submit.jsp │ └── static/ // CSS/JS/IMG 静态资源(可被直接访问) │ ├── css/ │ ├── js/ │ └── upload/ // 用户上传的图片存放目录(需 Tomcat context.xml 配置别名)

血泪经验:web.xml中<welcome-file-list>必须指向login.jsp,但不能直接访问http://localhost:8080/login.jsp—— 正确入口是http://localhost:8080/property/login(由LoginController处理),否则绕过登录校验。这是新手翻车最高频的漏洞。


3. 数据库设计:从 E-R 图到可执行 SQL 的 5 个关键决策

3.1 核心实体关系与字段取舍逻辑

小区物业系统看似简单,但字段设计稍有不慎就会引发连锁问题。以下是经过 3 次迭代后确定的 5 张主表及设计依据:

表名字段重点为什么这么设计不这么做的后果
t_owner(业主)idBIGINT PK AI,building_noVARCHAR(10),unit_noVARCHAR(10),room_noVARCHAR(10),phoneVARCHAR(20) NOT NULL,statusTINYINT DEFAULT 1(1=正常,0=冻结)building/unit/room拆开存,方便按楼栋统计入住率;phone设 NOT NULL 避免催费联系不上;status代替物理删除,防止工单关联丢失合并为room_code VARCHAR(20)导致无法按楼栋模糊查询;phone允许 NULL 导致缴费通知失败率飙升
t_fee(费用)id,owner_id,fee_typeENUM('water','electric','property','parking'),year_monthCHAR(6)(格式:202403),amountDECIMAL(10,2),paid_statusTINYINT(0=未缴,1=已缴),pay_timeDATETIME NULLyear_month用 CHAR(6) 而非 DATE,避免跨年分页时WHERE date >= '2023-01-01'扫描全表;paid_status+pay_time组合实现“已缴时间可查”,而非仅布尔值用fee_date DATE存,每月生成记录时需INSERT ... SELECT生成全量,性能差;paid_status用 BIT 导致 MySQL 8.0 查询报错
t_repair_order(报修)id,owner_id,titleVARCHAR(100),contentTEXT,statusTINYINT(0=待处理,1=处理中,2=已完成,3=已关闭),handler_idINT NULL(处理员工 ID),finish_timeDATETIME NULLstatus用数字枚举而非字符串,减少索引体积;handler_id允许 NULL(初始未分配),避免强制关联员工表导致插入失败status用'pending'/'processing'字符串,索引长度翻倍;handler_id设 NOT NULL 使业主提交报修时必须选员工,违背业务流程
t_notice(公告)id,titleVARCHAR(200),contentTEXT,publish_timeDATETIME DEFAULT CURRENT_TIMESTAMP,expire_timeDATETIME NULL(NULL=永不过期)expire_time允许 NULL,避免定时任务频繁扫描过期公告;publish_time设 DEFAULT 自动填充,省去 Controller 手动 setexpire_time设 NOT NULL DEFAULT '9999-12-31',导致过期逻辑难维护;publish_time由 Java new Date() 设置,时区不一致引发显示错误
t_user(系统用户)id,usernameVARCHAR(32) UNIQUE,passwordVARCHAR(128)(BCRYPT 加密),roleENUM('admin','staff','owner') NOT NULL,real_nameVARCHAR(20)password必须 BCRYPT 加密(BCryptPasswordEncoder.encode()),绝不用 MD5;role用 ENUM 而非外键,避免权限表过度设计password存明文或 MD5,审计直接一票否决;role用t_role表关联,增加 JOIN 开销且无必要

3.2 可直接执行的建表 SQL(含索引与注释)

复制粘贴到 MySQL 8.0 命令行即可运行,已验证字符集与引擎:

-- 创建数据库(utf8mb4 支持 emoji,但物业系统不用,故用 utf8) CREATE DATABASE IF NOT EXISTS property_db CHARACTER SET utf8 COLLATE utf8_general_ci; USE property_db; -- 业主表 CREATE TABLE t_owner ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', building_no VARCHAR(10) NOT NULL COMMENT '楼栋号', unit_no VARCHAR(10) NOT NULL COMMENT '单元号', room_no VARCHAR(10) NOT NULL COMMENT '房间号', name VARCHAR(20) NOT NULL COMMENT '业主姓名', phone VARCHAR(20) NOT NULL COMMENT '联系电话', id_card VARCHAR(18) COMMENT '身份证号', status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-冻结', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='业主信息表'; -- 添加复合索引:按楼栋+单元+房间快速定位 ALTER TABLE t_owner ADD INDEX idx_building_unit_room (building_no, unit_no, room_no); -- 费用表(注意 year_month 格式) CREATE TABLE t_fee ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', owner_id BIGINT NOT NULL COMMENT '业主ID', fee_type ENUM('water','electric','property','parking') NOT NULL COMMENT '费用类型', year_month CHAR(6) NOT NULL COMMENT '年月,格式YYYYMM', amount DECIMAL(10,2) NOT NULL COMMENT '金额', paid_status TINYINT DEFAULT 0 COMMENT '缴费状态:0-未缴,1-已缴', pay_time DATETIME NULL COMMENT '缴费时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', FOREIGN KEY (owner_id) REFERENCES t_owner(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='费用记录表'; -- 添加联合索引:按业主查历史缴费 + 按年月批量生成 ALTER TABLE t_fee ADD INDEX idx_owner_paid (owner_id, paid_status); ALTER TABLE t_fee ADD INDEX idx_yearmonth (year_month); -- 报修工单表 CREATE TABLE t_repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', owner_id BIGINT NOT NULL COMMENT '业主ID', title VARCHAR(100) NOT NULL COMMENT '标题', content TEXT NOT NULL COMMENT '内容', status TINYINT DEFAULT 0 COMMENT '状态:0-待处理,1-处理中,2-已完成,3-已关闭', handler_id INT NULL COMMENT '处理人ID(员工)', finish_time DATETIME NULL COMMENT '完成时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', FOREIGN KEY (owner_id) REFERENCES t_owner(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='报修工单表'; -- 添加索引:按状态查待办 + 按业主查历史 ALTER TABLE t_repair_order ADD INDEX idx_status (status); ALTER TABLE t_repair_order ADD INDEX idx_owner (owner_id); -- 公告表 CREATE TABLE t_notice ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', title VARCHAR(200) NOT NULL COMMENT '标题', content TEXT NOT NULL COMMENT '内容', publish_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', expire_time DATETIME NULL COMMENT '过期时间(NULL=永不过期)', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='公告信息表'; -- 系统用户表(含密码加密字段) CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(128) NOT NULL COMMENT 'BCRYPT 加密密码', role ENUM('admin','staff','owner') NOT NULL COMMENT '角色', real_name VARCHAR(20) NOT NULL COMMENT '真实姓名', phone VARCHAR(20) COMMENT '联系电话', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系统用户表';

参数说明:

  • ENGINE=InnoDB:必须,支持事务和外键;
  • CHARSET=utf8:MySQL 8.0 中utf8实际是utf8mb3,足够覆盖中文,比utf8mb4节省空间;
  • ON DELETE CASCADE:业主删除时自动清理其费用和报修记录,避免孤儿数据;
  • idx_building_unit_room:三字段联合索引,查询SELECT * FROM t_owner WHERE building_no='1' AND unit_no='2'时走索引,避免全表扫描。

4. 登录与权限控制:Filter 拦截的 3 层防线

4.1 web.xml 中的 Filter 配置(顺序即拦截顺序)

Filter 是 JavaWeb 权限控制的基石,顺序错了整个链路就崩。web.xml中必须按此顺序声明:

<!-- 1. 字符编码过滤器(最前,确保后续所有请求参数 UTF-8) --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>com.example.property.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 2. 登录状态检查过滤器(拦截所有 /admin/* 和 /staff/* 路径) --> <filter> <filter-name>LoginCheckFilter</filter-name> <filter-class>com.example.property.filter.LoginCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/staff/*</url-pattern> </filter-mapping> <!-- 3. 静态资源放行过滤器(最后,避免拦截 CSS/JS) --> <filter> <filter-name>StaticResourceFilter</filter-name> <filter-class>com.example.property.filter.StaticResourceFilter</filter-class> </filter> <filter-mapping> <filter-name>StaticResourceFilter</filter-name> <url-pattern>/static/*</url-pattern> </filter-mapping>

注意:<url-pattern>用/*会拦截所有请求,包括/login本身,导致死循环。所以LoginCheckFilter只配/admin/*和/staff/*,登录页/login和/logout由 Controller 直接处理,不经过该 Filter。

4.2 LoginCheckFilter 的核心逻辑(含 Session 失效处理)

public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 1. 获取当前请求路径 String path = req.getServletPath(); // 2. 放行登录页、登出页、验证码、静态资源(即使没登录也能访问) if ("/login".equals(path) || "/doLogin".equals(path) || "/logout".equals(path) || "/captcha".equals(path)) { chain.doFilter(request, response); return; } // 3. 检查 Session 中是否存在 user 对象 HttpSession session = req.getSession(false); // false 表示不创建新 Session User user = (User) session != null ? session.getAttribute("user") : null; // 4. Session 失效或 user 为空,则重定向到登录页 if (session == null || user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 5. 角色校验:admin 可访问所有 /admin/*,staff 只能访问部分 /staff/*(如报修处理,不能改费用) String role = user.getRole(); if (path.startsWith("/admin/") && !"admin".equals(role)) { req.setAttribute("msg", "权限不足,仅管理员可访问"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } if (path.startsWith("/staff/") && !"staff".equals(role)) { req.setAttribute("msg", "权限不足,仅工作人员可访问"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } // 6. 通过校验,放行 chain.doFilter(request, response); } }

关键参数说明:

  • req.getSession(false):不主动创建 Session,避免未登录用户也生成 Session ID,浪费内存;
  • session.getAttribute("user"):存储在 Session 中的 User 对象必须是序列化对象(implements Serializable),否则集群环境下反序列化失败;
  • 角色校验放在 Filter 而非 Controller,避免每个 Controller 方法重复写if (!"admin".equals(user.getRole()));
  • 重定向用resp.sendRedirect()而非request.getRequestDispatcher().forward(),确保 URL 地址栏变为/login.jsp,防止用户刷新后重复提交。

4.3 登录 Controller 的安全实践(防暴力破解)

@WebServlet("/doLogin") public class LoginController extends HttpServlet { private final UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); String captcha = req.getParameter("captcha"); // 1. 验证码校验(必须,否则自动化脚本暴力破解) HttpSession session = req.getSession(); String sessionCaptcha = (String) session.getAttribute("captcha"); if (!captcha.equalsIgnoreCase(sessionCaptcha)) { req.setAttribute("msg", "验证码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 2. 用户名密码校验(BCRYPT 加密比对) User user = userService.login(username, password); if (user == null) { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 3. 登录成功:清除验证码(防重用),存入 Session,跳转首页 session.removeAttribute("captcha"); // 清除验证码,防止重复使用 session.setAttribute("user", user); // 存入 Session session.setMaxInactiveInterval(1800); // Session 30 分钟无操作失效 // 4. 根据角色跳转不同首页 String redirectUrl = "admin".equals(user.getRole()) ? "/admin/index.jsp" : "/staff/index.jsp"; resp.sendRedirect(req.getContextPath() + redirectUrl); } }

避坑点:session.setMaxInactiveInterval(1800)必须在setAttribute后设置,否则无效;验证码校验必须用equalsIgnoreCase(),忽略大小写,避免用户输入AbC与生成的abc不匹配。


5. 避坑:开发与部署阶段的 5 个高频翻车点

5.1 现象:Tomcat 启动后访问http://localhost:8080/property报 404

原因:项目未正确部署为property上下文路径,或web.xml中<servlet-mapping>的url-pattern与实际请求路径不匹配。
解决:

  • 检查 IDEA 中 Tomcat 配置:Deployment→Application context必须设为/property(不能是/或空);
  • 检查web.xml中所有<servlet>的<url-pattern>,例如LoginController的映射必须是<url-pattern>/login</url-pattern>,则访问路径为http://localhost:8080/property/login;
  • 若用 Maven 打包 WAR,确认pom.xml中<finalName>property</finalName>,否则生成property-1.0-SNAPSHOT.war,部署后路径为/property-1.0-SNAPSHOT。

5.2 现象:JSP 页面中<c:forEach>标签不解析,原样输出 HTML

原因:JSTL 标签库未正确加载,或web.xml的web-app版本声明过低。
解决:

  • 确认pom.xml已引入jstl:1.2依赖;
  • 检查web.xml头部声明必须为 Servlet 4.0:
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
    若写成web-app_3_0.xsd,JSTL 1.2 不被识别;
  • JSP 页面顶部必须有<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。

5.3 现象:MySQL 连接报Communications link failure

原因:MySQL 8.0 默认关闭远程连接,且bind-address为127.0.0.1,本地 Java 应用连接时被拒绝。
解决:

  • 修改 MySQL 配置文件my.cnf(Windows 为my.ini):
    [mysqld] bind-address = 0.0.0.0 # 允许所有 IP 连接 port = 3306
  • 重启 MySQL 服务;
  • 执行 SQL 授权:GRANT ALL PRIVILEGES ON property_db.* TO 'root'@'%' IDENTIFIED BY 'yourpwd'; FLUSH PRIVILEGES;;
  • Java 连接 URL 改为:jdbc:mysql://127.0.0.1:3306/property_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true。

5.4 现象:文件上传后图片无法在页面显示,路径http://localhost:8080/property/upload/1.jpg404

原因:Tomcat 默认不提供webapp/upload/目录的静态资源访问,需配置虚拟路径。
解决:

  • 在conf/context.xml中<Context>标签下添加:
    <Resources className="org.apache.naming.resources.FileDirContext" base="D:/workspace/property/src/main/webapp/upload" docBase="/upload" />
  • 或更简单:将上传目录设为webapp/static/upload,前端直接访问/static/upload/1.jpg(因/static/*已在 Filter 中放行);
  • 代码中保存文件时,路径必须用getServletContext().getRealPath("/static/upload/")获取绝对路径,而非硬编码D:/...。

5.5 现象:缴费列表分页时,第 2 页数据重复第 1 页,或总数不准

原因:DAO 层分页 SQL 写错,用了LIMIT 10,10但未配合ORDER BY,导致 MySQL 优化器返回顺序不稳定。
解决:

  • 所有分页查询必须带ORDER BY,推荐用主键:
    SELECT * FROM t_fee WHERE owner_id = ? ORDER BY id DESC LIMIT ?, ?
  • Java 中计算 offset:int offset = (pageNum - 1) * pageSize;;
  • 总数查询独立写:SELECT COUNT(*) FROM t_fee WHERE owner_id = ?,不要用SELECT COUNT(*) OVER()(MySQL 8.0 才支持,且性能差);
  • 避免在 Service 层用List.subList()做内存分页,大数据量直接 OOM。

6. 验证与交付:让系统真正“跑起来”的 3 个硬指标

6.1 本地可验证的 5 分钟冒烟测试清单

别等全部功能做完才测试,每完成一个模块就跑一次。以下 5 步能在 5 分钟内验证核心链路是否打通:

步骤操作预期结果失败定位点
1. 启动验证启动 Tomcat,观察控制台无SEVERE错误,末尾出现Server startup in [xxx] msTomcat 成功启动web.xml语法错误、Servlet 类路径错误、JDBC 驱动未加载
2. 登录验证浏览器访问http://localhost:8080/property/login.jsp→ 输入 admin/admin → 点登录跳转至/admin/index.jsp,页面显示“欢迎,管理员”LoginController.doPost()逻辑错误、BCRYPT 密码比对失败、Session 未存user
3. 数据验证在/admin/owner_add.jsp添加业主 → 提交 → 查看/admin/owner_list.jsp新增业主出现在列表,building_no/unit_no/room_no显示正确OwnerDao.add()SQL 插入失败、t_owner表字段类型与 Java 实体不匹配(如BIGINTvsint)
4. 文件验证在/staff/repair_submit.jsp提交报修(带图片)→ 查看/admin/repair_list.jsp报修单显示标题、内容、图片缩略图(<img src="/static/upload/xxx.jpg">)RepairController文件保存路径错误、StaticResourceFilter未放行/static/*
5. 查询验证在/admin/fee_generate.jsp选择 202403 → 生成当月费用 → 查看/admin/fee_list.jsp列表显示该月所有业主费用,paid_status=0,amount金额正确FeeService.generateMonthFee()未遍历所有业主、t_fee表year_month字段存为2024-03(应为202403)

提示:每次测试前清空浏览器缓存(Ctrl+F5),避免旧 JS/CSS 导致界面异常;用 Chrome DevTools 的 Network 标签查看 AJAX 请求是否 200,Response 是否返回 JSON 数据。

6.2 导出 Excel 的实战代码(Apache POI 5.2.4)

缴费明细需导出 Excel,用 POI 比 JSP 输出 CSV 更可靠。FeeController中新增导出方法:

@WebServlet("/exportFee") public class ExportFeeController extends HttpServlet { private final FeeService feeService = new FeeService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String yearMonth = req.getParameter("yearMonth"); // 前端传 202403 List<Fee> feeList = feeService.listByYearMonth(yearMonth); // 1. 创建工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("缴费明细-" + yearMonth); // 2. 写入表头 String[] headers = {"业主姓名", "房间号", "费用类型", "金额", "缴费状态", "缴费时间"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 3. 写入数据 int rowNum = 1; for (Fee fee : feeList) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(fee.getOwnerName()); // 从关联查询获取 row.createCell(1).setCellValue(fee.getRoomNo()); row.createCell(2).setCellValue(fee.getFeeType()); row.createCell(3).setCellValue(fee.getAmount()); row.createCell(4).setCellValue(fee.getPaidStatus() == 1 ? "已缴" : "未缴"); row.createCell(5).setCellValue(fee.getPayTime() != null ? new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(fee.getPayTime()) : ""); } // 4. 设置响应头,触发下载 resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=fee_" + yearMonth + ".xlsx"); // 5. 写出流 try (OutputStream out = resp.getOutputStream()) { workbook.write(out); } finally { workbook.close(); } } }

关键参数说明:

  • XSSFWorkbook:对应.xlsx格式,兼容 Excel 2007+;若用HSSFWorkbook(.xls),中文会乱码且最大行数 65536;
  • setContentType和Content-Disposition缺一不可,否则浏览器直接打开 Excel 而非下载;
  • workbook.close()必须在finally块中,防止内存泄漏;
  • 时间格式化用SimpleDateFormat,避免LocalDateTime.toString()输出 ISO 格式(如2024-03-15T10:30:00)。

6.3 部署到阿里云轻量应用服务器的 3 步实操

学生作业常卡在“本地能跑,服务器跑不了”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询