简介:这是一套基于SSM框架的小区人口管理系统毕业设计程序,适合计算机相关专业学生用于毕业设计、课程设计或项目实践。系统采用Java技术实现,覆盖小区住户管理、费用信息管理、出入登记、疫情黑名单等核心功能,开发流程包含需求分析、总体设计、详细设计、数据库实现与功能测试等环节,结构完整,便于参考与二次开发。压缩包共305个文件,大小约1.04MB,主要包含Java源码、class文件、Vue页面、XML配置、SQL数据库脚本、properties配置文件等,后端逻辑、前端界面与数据库初始化材料一应俱全。包内附带源代码和数据库,并配有环境配置说明,解压后可按说明部署运行;已有153人浏览学习,适合需要快速搭建可运行毕设项目、理解SSM开发流程的读者。
1. SSM小区人口管理系统:一套能直接跑起来的毕业设计方案
对于计算机相关专业的学生来说,毕业设计选管理系统类题目是最稳妥的,但“SSM小区人口管理系统”恰恰是容易翻车的那一类:框架版本不兼容、Mapper XML 扫不到、中文乱码、Tomcat 404,任何一个坑都能让你多熬两个通宵。这套资源把 Spring、SpringMVC、MyBatis 三个框架串成一个完整可运行的 Web 工程,覆盖住户档案、流动人口、楼栋房屋、访客登记、系统权限、统计报表六个模块,对应 Java Web 毕业设计里大部分高频采分点。适合想快速搭出一个能演示、能答辩、底层逻辑说得清的管理系统的人。下文我会从技术选型讲到配置文件、核心代码和排错思路,你照着做就能把项目拉起来。
2. 技术选型剖析:为什么 SSM 组合适合小区人口管理
2.1 三个框架的分工:IoC、路由、SQL 映射
SSM 是三件套:Spring 管对象,SpringMVC 管请求分发,MyBatis 管数据库操作。写毕业设计时,最关键的是能跟评委说清楚“为什么这么拆”。
Spring 的 IoC 容器负责创建和管理业务对象。传统写法是手动new一个对象再注入依赖,Spring 用依赖注入机制把这个过程统一交给容器。好处是:当你需要改数据源配置时,只需改一行jdbc.properties,业务代码不用动。在小区人口管理场景里,住户服务、楼栋服务、访客服务都是容器管理的 Bean,各自只关注自己的业务逻辑。
SpringMVC 是 Spring 家族的 Web 模块,核心组件是 DispatcherServlet。浏览器发一个/resident/list请求进来,DispatcherServlet 根据@RequestMapping找到对应的 Controller 方法,调用 Service 处理业务,最终返回 JSP 页面或 JSON 数据。答辩时这段请求链路经常被问,你要能像复述源码一样说清楚:请求进 Filter,再进 DispatcherServlet,HandlerMapping 找到 Handler,HandlerAdapter 执行方法,最后视图解析器渲染响应。
MyBatis 的作用是 Java 对象和 SQL 参数之间的映射。相比手写 JDBC,MyBatis 的核心价值在于动态 SQL。比如按条件查住户时,姓名可能传也可能不传,楼栋 ID 可能传也可能不传,用<if>标签拼条件可以避免大量 if-else,同时#{}预编译参数天然防 SQL 注入。
2.2 为什么不用 Spring Boot:框架组合更能暴露底层逻辑
Spring Boot 的开发效率明显更高,但毕业设计的评委会更想看到你对底层原理的理解。SSM 要求你亲手写 XML、配数据源、配 Mapper 扫描路径,这个过程本身就是采分点。常见的做法是项目分controller、service、dao、entity四层,数据库用 MySQL 8.0,连接池用 C3P0 或 Druid,前端用 JSP + jQuery + Bootstrap。
我一般会在答辩时说:这个项目不是简单地套框架,而是在理解 IoC 解决对象管理问题、AOP 解决切面问题之后,再把它们落进具体模块。例如事务管理使用 Spring 声明式事务,在applicationContext.xml里配置tx:advice,住户登记涉及多次写操作,任何一个失败就整体回滚,避免数据不一致。
Spring Boot 当然也不是不能用。但如果课题名称就是 SSM,建议还是按 SSM 来做。不少学校对课题与框架的对应关系有隐性要求,临时换成 Spring Boot 反而容易在开题和答辩环节被问住。
2.3 六个模块的数据流依赖
系统功能上划分为住户档案、流动人口、楼栋房屋、访客登记、系统用户与权限、统计报表六个模块。它们不是平级的,依赖顺序很清晰。
楼栋房屋是基础数据,得先建出来,因为住户必须挂在具体的楼栋、单元、房间下。一个楼栋有若干单元,每个单元有若干层,每个房间用room_type区分住宅或商铺。t_room是住户表的依赖项,resident表里的room_id外键指向它。
住户档案是整个系统的核心,字段包含姓名、性别、身份证号、联系电话、户籍地址、入住时间、与户主关系。流动人口不绑房间,只登记来源地、暂住证号、预计居住时长,适配租户场景。访客登记记录进出事件,绑定住户或房间,进出时间都必须记录。权限模块控制谁能查看全部数据、谁能执行增删改查。整套数据流方向是楼栋到房间再到住户,流动人口和访客是侧挂的两条支线,报表模块从所有表中聚合数据出图。
3. 环境搭建与数据库初始化:九张表和三个配置文件的落地顺序
3.1 数据库设计:九张表与关键字段
先看数据库设计。以某小区为例,库名community_db,字符集utf8mb4,排序规则utf8mb4_general_ci。核心表一共九张,我列一个表说明用途和关键字段:
| 表名 | 用途 | 关键字段 |
|---|---|---|
t_building | 楼栋基础信息 | building_no,unit_count,floor_count |
t_room | 房间与房屋类型 | building_id,room_no,room_type,area,living_status |
t_resident | 常住人口档案 | room_id,name,id_card,phone,checkin_date |
t_flowing | 流动人口登记 | name,id_card,origin_addr,temp_permit_no,expect_days |
t_visitor | 访客进出记录 | resident_id,visitor_name,visit_time,leave_time,reason |
t_user | 系统登录用户 | username,password,role_id,status |
t_role | 角色定义 | role_name,role_desc |
t_menu | 菜单权限项 | menu_name,url,parent_id |
t_role_menu | 角色菜单关联 | role_id,menu_id |
建表脚本在资源包sql/init.sql里,用 MySQL 客户端或图形化工具直接导入即可。这里提前说两个字段坑。
第一个坑:身份证号用VARCHAR(20),手机号用VARCHAR(15),千万不要用数字类型。身份证号 18 位,超过 INT 范围;手机号如果存成 INT,特殊场景会丢前导零。第二个坑:时间字段checkin_date设置DEFAULT CURRENT_TIMESTAMP,插入的时候不传值也能自动填上,减少前端传空值导致的 SQL 错误。
3.2 Maven 项目骨架与依赖版本
工程采用标准 Maven war 包结构:
ssm-community ├── pom.xml ├── src/main/java/com/community │ ├── controller │ ├── service │ ├── mapper │ ├── entity │ └── util ├── src/main/resources │ ├── jdbc.properties │ ├── applicationContext.xml │ ├── springmvc.xml │ ├── mybatis-config.xml │ └── mapper/*.xml ├── src/main/webapp │ ├── WEB-INF/web.xml │ ├── static │ └── WEB-INF/jsp └── sql/init.sqlJVM 选 1.8,Tomcat 选 8.5 或 9.0。pom.xml 里的依赖组合我建议固定在这一套:
<properties> <spring.version>5.2.22.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.5</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>版本选型的逻辑:Spring 5.2.x 在 Tomcat 8.5/9.0 下的兼容性最稳,没有高版本 JDK 下才会出现的缺类问题;MyBatis 3.5.x 的动态 SQL 和@Param注解支持已经很成熟;MySQL 8.0 连接驱动必须配serverTimezone,这在下一节展开。
3.3 Spring 与 MyBatis 集成配置:三个最容易写错的地方
配置层面先看jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/community_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456 jdbc.initialPoolSize=5 jdbc.minPoolSize=5 jdbc.maxPoolSize=30 jdbc.acquireIncrement=5URL 参数不是玄学,每一个都有明确的坑位。serverTimezone=Asia/Shanghai不配,MySQL 8.0 驱动会直接报时区异常。characterEncoding=utf8不配,中文写入数据库会变成问号。useSSL=false是关闭 SSL 握手,本地开发不加会有大量红色告警刷屏。连接池的三个参数控制的是空闲连接预热和扩容步长,maxPoolSize=30对单机毕业设计足够,调太大会增加 MySQL 端连接数开销。
再看springmvc.xml:
<context:component-scan base-package="com.community.controller" /> <mvc:annotation-driven /> <mvc:resources mapping="/static/**" location="/static/" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>视图解析器把 Controller 返回的字符串解析成物理 JSP 路径。比如 Controller 返回"resident/list",最终渲染的就是/WEB-INF/jsp/resident/list.jsp。JSP 页面放在WEB-INF目录下有两个实际好处:一是外部不能通过 URL 直接访问页面源码,所有请求必须经过 Controller 跳转;二是路径集中管理,页面前后移动不用改 Controller 代码,只改 prefix 就行。
然后是applicationContext.xml,这里集中了数据源、SqlSessionFactory、Mapper 扫描和事务管理器:
<context:property-placeholder location="classpath:jdbc.properties" /> <context:component-scan base-package="com.community.service" /> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="user" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="initialPoolSize" value="${jdbc.initialPoolSize}" /> <property name="maxPoolSize" value="${jdbc.maxPoolSize}" /> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:mybatis-config.xml" /> <property name="mapperLocations" value="classpath:mapper/*.xml" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.community.mapper" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED" /> <tx:method name="*" read-only="true" /> </tx:attributes> </tx:advice>mapperLocations的路径是高频报错点。写classpath:mapper/*.xml就够了,不要写classpath*:/mapper/**/*.xml,后者在多模块或包含多个 jar 时可能扫描到重复资源,导致 mapper 重复注册。MapperScannerConfigurer会扫描com.community.mapper包下所有接口,自动生成代理对象,业务层直接@Autowired注入接口即可。
事务配置里有一个容易忽略的细节:add*、update*、delete*方法走事务,read-only=true的查询方法不走写事务。Service 方法命名必须匹配这里的前缀,如果方法叫saveResident,开头不是add,事务就不会生效。习惯上 Service 方法我统一叫add、update、delete开头,就是为了和事务切点对齐。
4. 核心模块实现:住户登记、组合查询与报表统计
4.1 住户登记的 Controller-Service-Mapper 链路
住户登记是整套系统的核心流程,用它来演示完整的链路。前端 JSP 表单提交/resident/add,Controller 接收参数后调用 Service,Service 做业务校验,Mapper 写库。
先看 Controller:
@Controller @RequestMapping("/resident") public class ResidentController { @Autowired private ResidentService residentService; @RequestMapping("/add") public String add(Resident resident, Model model) { int code = residentService.addResident(resident); if (code == 1) { return "redirect:/resident/list"; } model.addAttribute("error", "身份证号已存在或手机号格式不正确"); return "resident/add"; } }Controller 层的两个关键点:@Autowired注入的是 Service 接口,不是实现类,底层由 Spring 自动装配;redirect:/resident/list是重定向写法,避免用户刷新页面时重复提交表单。重定向后 URL 会变成/resident/list,Post-Redirect-Get 模式是 Web 开发里的老规矩,答辩时值得提一句。
Service 层负责校验和幂等控制:
@Service public class ResidentServiceImpl implements ResidentService { @Autowired private ResidentMapper residentMapper; @Override public int addResident(Resident resident) { // 身份证号重复校验,避免插入时抛唯一键异常 if (residentMapper.countByIdCard(resident.getIdCard()) > 0) { return 0; } // 手机号正则校验,简单格式检查 if (!resident.getPhone().matches("^1[3456789]\\d{9}$")) { return -1; } return residentMapper.insertResident(resident); } }其中countByIdCard在 Mapper 接口里是一个独立查询方法,SQL 用SELECT COUNT(*) FROM t_resident WHERE id_card = #{idCard}。为什么要先查再插?因为数据库有唯一索引时,重复插入会抛DuplicateKeyException,如果不捕获,页面会直接 500。先查一次虽然多一条 SQL,但能拿到明确的业务错误码,用户体验更可控。
Mapper XML 里的插入语句:
<insert id="insertResident" parameterType="com.community.entity.Resident" useGeneratedKeys="true" keyProperty="residentId"> INSERT INTO t_resident (room_id, name, gender, id_card, phone, household_addr, checkin_date, relation_type) VALUES (#{roomId}, #{name}, #{gender}, #{idCard}, #{phone}, #{householdAddr}, #{checkinDate}, #{relationType}) </insert>useGeneratedKeys="true"和keyProperty="residentId"组成主键回填,插入成功后数据库自增 ID 会写回resident对象的residentId字段。没有这个配置,后续想拿新 ID 就只能再查一次。
4.2 多条件组合查询:动态 SQL 的四个边界条件
人口管理系统的核心场景是“筛人”:按姓名模糊查、按楼栋查、按性别查、按入住时间段查、按房屋类型查。这些条件任意组合,空值不参与过滤,动态 SQL 就是为这个场景设计的。
<select id="selectResidentByCondition" resultType="com.community.entity.Resident"> SELECT r.* FROM t_resident r LEFT JOIN t_room rm ON r.room_id = rm.room_id WHERE 1 = 1 <if test="name != null and name != ''"> AND r.name LIKE CONCAT('%', #{name}, '%') </if> <if test="buildingId != null and buildingId != ''"> AND rm.building_id = #{buildingId} </if> <if test="gender != null and gender != ''"> AND r.gender = #{gender} </if> <if test="checkinStart != null and checkinStart != ''"> AND r.checkin_date >= #{checkinStart} </if> <if test="checkinEnd != null and checkinEnd != ''"> AND r.checkin_date <= #{checkinEnd} </if> <if test="roomType != null and roomType != ''"> AND rm.room_type = #{roomType} </if> ORDER BY r.checkin_date DESC </select>这段 SQL 有四个边界要注意。第一,WHERE 1 = 1是动态拼接的惯用法,保证所有<if>条件都能用AND开头,不用管谁是第一个条件。第二,#{name}是预编译占位符,不是字符串拼接,能防 SQL 注入;而${name}是直接替换,实战中禁止用于用户输入。第三,模糊匹配用CONCAT('%', #{name}, '%'),避免在业务代码里手动拼百分号,统一收敛在 SQL 层。第四,XML 里<必须转义成<,>可以保留,否则 XML 解析阶段就会报错,页面都起不来。
Controller 接收查询参数时,一般用一个独立的查询对象ResidentQuery接收所有筛选字段,比在方法里写十几个参数更干净。批量操作的 Mapper 方法用@Param区分参数名:
List<Resident> selectResidentByCondition(@Param("query") ResidentQuery query);对应 XML 里的取值就变成#{query.name}。这是我踩出来的经验:早期不拆查询对象,直接在控制层堆了七八个形参,后来加一个筛选条件就要动四层代码,非常痛苦。
4.3 报表统计:单条 SQL 聚合三张表
统计报表模块能直接拉开和“玩具系统”的差距。按楼栋统计常住人口:
<select id="countResidentByBuilding" resultType="map"> SELECT b.building_no, COUNT(r.resident_id) AS resident_count FROM t_building b LEFT JOIN t_room rm ON b.building_id = rm.building_id LEFT JOIN t_resident r ON rm.room_id = r.room_id GROUP BY b.building_id, b.building_no ORDER BY resident_count DESC </select>这里用了两次 LEFT JOIN,把所有房间都带上,即使某些房间没有住户,人数也是 0,不会出现楼栋消失的情况。返回类型写成map,MyBatis 会把每行变成一个 Map,键是列的别名building_no和resident_count。Controller 端直接返回 JSON:
@RequestMapping("/residentByBuilding") @ResponseBody public List<Map<String, Object>> residentByBuilding() { return statisticService.countResidentByBuilding(); }注意@ResponseBody把返回对象序列化成 JSON,前置条件是 pom 里有 Jackson 依赖。如果没有加 Jackson,SpringMVC 会因为没有可用的消息转换器而直接报HttpMediaNotAcceptableException。
5. 常见问题排查:项目跑不起来的五个典型报错点
这一章全是毕业设计过程中真实翻车记录的沉淀。每条按“现象 → 原因 → 解决”整理,你在本地启动遇到同样的报错,可以直接对照处理。
5.1 现象:Tomcat 启动正常,访问任意 Controller 返回 404
Tomcat 能起,日志没有异常,但访问http://localhost:8080/resident/list返回 404。
原因大概率是 DispatcherServlet 的拦截路径配置不对,或者web.xml里配置的 servlet 映射和实际访问路径不一致。如果<url-pattern>写成了/*,SpringMVC 连 JSP 都要拦截,Controller 虽然能找到,但视图解析返回的 JSP 会被再次当作请求处理,最终 404。
解决方式:web.xml中 DispatcherServlet 的 url-pattern 配成/,表示所有非 JSP 请求都交给 Spring 处理;同时确认springmvc.xml里有<mvc:annotation-driven />,否则@RequestMapping不会被注册。视图解析器的 prefix 必须是/WEB-INF/jsp/,不要写成/jsp/,否则也找不到页面。
5.2 现象:MyBatis 报Invalid bound statement (not found)
启动或调用 Mapper 方法时,抛出org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.community.mapper.ResidentMapper.selectResidentByCondition。
原因有三个:一是 Mapper 接口和 XML 的 namespace 不一致;二是 XML 中<select>的 id 和接口方法名不一致;三是applicationContext.xml中mapperLocations路径扫不到 XML 文件。
解决方式:打开 Mapper 接口,确认方法名selectResidentByCondition和 XML 里<select id="selectResidentByCondition">完全一致;namespace 必须是接口的全限定名com.community.mapper.ResidentMapper;mapperLocations保持classpath:mapper/*.xml,XML 文件放在resources/mapper目录下。改完配置后,在 IDEA 里执行Build → Rebuild Project,有时候只是编译输出目录里没有同步 XML。
5.3 现象:页面显示正常,数据库里中文变成问号
前端输入中文可以正常展示,但数据库里存进去的是???,或者反过来数据库有中文但页面显示乱码。
原因分三层:数据库连接串没带characterEncoding=utf8,MySQL 服务端和客户端之间按系统默认编码传输;JSP 页面和响应没声明 UTF-8;Tomcat 8.5 以下版本 POST 请求默认编码是 ISO-8859-1。
解决方式三层一起改,缺一层都会继续乱。第一层jdbc.properties的 URL 加useUnicode=true&characterEncoding=utf8;第二层在web.xml配置CharacterEncodingFilter,encoding 设置UTF-8,url-pattern 设置/*;第三层建表语句统一DEFAULT CHARSET=utf8mb4。如果你用的是 MySQL 8.0,连接串里同时带上serverTimezone=Asia/Shanghai,否则时区异常会直接让你启动失败。
5.4 现象:C3P0 连接池报Connection is closed或请求超时
系统运行一段时间后,页面操作突然报数据库连接异常,重启 Tomcat 又恢复正常,反复发生。
原因是 MySQL 的wait_timeout默认 8 小时,C3P0 池里空闲超过 8 小时的连接被服务端断开,而连接池并不知道,仍然把这些失效连接交给业务层,拿到手再用就会抛Connection is closed。
解决方式:在jdbc.properties里给 C3P0 增加连接回收检测参数,比如设置testConnectionOnCheckout=true和preferredTestQuery=SELECT 1,每次从连接池取连接时验证有效性;或者把空闲连接回收器打开,idleConnectionTestPeriod=60。如果觉得 C3P0 麻烦,可以直接换 Druid,testWhileIdle、keepAlive参数天然处理这种问题,性能监控页面还能在答辩时当附加功能展示。
5.5 现象:前端 Ajax 提交 JSON,Controller 接收到的对象全是 null
页面用了$.ajax({ contentType: 'application/json', data: JSON.stringify(obj) }),后端 Controller 方法直接写Resident resident,断点看到所有字段都是 null。
原因是 SpringMVC 默认的消息转换器不会把请求体 JSON 自动绑到普通方法参数上,不加@RequestBody时它按表单参数绑定,自然拿不到 JSON 里的字段。
解决方式:在参数前加@RequestBody:
@RequestMapping("/save") @ResponseBody public Result save(@RequestBody Resident resident) { return residentService.save(resident); }同时确保 pom.xml 里有 Jackson 依赖。如果没有jackson-databind,SpringMVC 在解析@RequestBody时会抛HttpMessageNotReadableException,报错信息会明确提示缺转换器。前端请求的contentType也必须是application/json;charset=UTF-8,不是默认的application/x-www-form-urlencoded。
6. 答辩加分项:用 ECharts 和 POI 让数据自己说话
6.1 数据可视化:单页图表展示全小区实时状态
写一个dashboard.jsp,页面加载时用 Ajax 请求/statistics/residentByBuilding,拿到楼栋人数聚合数据后用 ECharts 渲染柱状图。这个功能已经成为我项目里的标配,因为它把“数据”和“决策”直接连起来,答辩时评委看到的不再是一堆表格。
$.ajax({ url: '/statistics/residentByBuilding', type: 'GET', dataType: 'json', success: function (data) { var chart = echarts.init(document.getElementById('buildingChart')); var option = { tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.map(item => item.building_no) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.map(item => item.resident_count) }] }; chart.setOption(option); } });后端 Controller 用@ResponseBody返回List<Map<String, Object>>,Jackson 自动把它转成 JSON 数组,前端data就是可用的对象数组。如果要做饼图,把series.type从bar改成pie,再给data配name和value就行。图表展示粒度可以从“按楼栋”提升到“按房屋类型”,SQL 的GROUP BY改一列,前端不用动。
6.2 数据导出:POI 生成 Excel,答辩必备功能
导出是另一个答辩亮点。住户列表导出成 Excel,后台一次写入,前端一个<a>标签链接就能下载:
@RequestMapping("/export") public void export(HttpServletResponse response) throws IOException { List<Resident> list = residentService.listAllResident(); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("常住人口"); String[] headers = {"姓名", "性别", "身份证号", "联系电话", "入住时间"}; Row head = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { head.createCell(i).setCellValue(headers[i]); } for (int i = 0; i < list.size(); i++) { Row row = sheet.createRow(i + 1); Resident r = list.get(i); row.createCell(0).setCellValue(r.getName()); row.createCell(1).setCellValue(r.getGender()); row.createCell(2).setCellValue(r.getIdCard()); row.createCell(3).setCellValue(r.getPhone()); row.createCell(4).setCellValue(r.getCheckinDate().toString()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=resident.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }两个细节要认真对待:Content-Type必须匹配 xlsx 的 MIME 类型,否则浏览器可能把它当成 HTML 打开;workbook.close()一定要执行,否则释放不了临时文件。文件名如果含中文,要做 URL 编码,不处理下载下来就是乱码文件名。
答辩时与其逐行念代码,不如现场走一遍:新增住户、故意输错身份证号看系统挡下来、按楼栋筛选、打开统计图表、导出 Excel。我早期带过的一个项目就是只做了 CRUD 就上台,被评委问“你的数据统计在哪里”时冷场了。从那以后我每次做管理系统,都强制把可视化和导出纳入开发链路,先把数据展示做了,再回头补边角功能。这条习惯帮我避开过不止一次答辩危机,希望帮到你。
本文还有配套的精品资源,点击获取