news 2026/11/6 5:29:34

基于SSM的小区人口管理系统:三级台账、RBAC与工程避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SSM的小区人口管理系统:三级台账、RBAC与工程避坑

简介:这份SSM小区人口管理系统毕业设计程序面向高校计算机相关专业学生与Java Web初学者,围绕住户信息、出入登记、费用信息和疫情黑名单等社区管理业务展开,可用于毕业设计、课程设计及管理系统实训参考。压缩包共305个文件、约1.04MB,涵盖Java源码、class文件、Vue前端页面、XML配置、SQL数据库脚本、properties配置及图片样式等,前后端分层清楚,方便按模块阅读与二次修改;用户权限、信息录入、查询统计和后台管理等环节均有对应实现,可借此梳理功能设计、结构设计与数据库设计之间的对应关系。已有155人学习,资源内含可运行源代码、数据库文件和环境配置说明,能帮助读者快速搭建环境、理解需求分析与总体设计,并对照功能模块实现、数据库访问代码和测试过程完成自己的毕设方案,也可为定制开发和排错调试提供参考。

1. 物业台账搬到线上:ssm小区人口管理系统解决的是哪类麻烦

小区物业办公室里最常见的一幕:一台电脑开着三四个 Excel,一个表记楼栋房屋,一个表记常住人口,还有一个表记租客和迁出记录,谁家老人独居、哪户是群租、哪套房子空置半年,全靠经办人在备注列里敲几行字。人一换,台账就断档。ssm小区人口管理系统干的活就是把这套台账搬进 Web 端,用楼栋—房屋—人口三级结构把「人住哪儿」这件事固定下来,再加上迁入迁出流水、访客登记和按楼栋统计,让数据不再散落在个人电脑里。这套基于 SSM 的实现适合三类人:正在做毕业设计、需要一份能跑通带界面的完整 CRUD 工程的学生;刚接触 Spring + MyBatis 整合、想看清分层怎么落地的初级开发;以及要给社区、园区做轻量级人员台账的开发者。它不追求高并发,追求的是表结构清楚、增删改查闭环、权限能拦住越权操作。

2. 技术栈与工程骨架:一份 SSM 工程的分层账本

先给结论:这套系统的技术选型是典型的「老而稳」组合,Spring 5 负责容器和事务,SpringMVC 负责请求分发,MyBatis 负责 SQL 与对象映射,MySQL 存数据,前端用 JSP + Bootstrap + jQuery 渲染,打成 war 包丢进 Tomcat 跑。这套栈放在今天不算时髦,但对毕设和中小型内部系统来说有个实打实的好处——每一层都能看到源码,出了问题不用对着自动配置猜。持久层没有用 JPA,是因为小区人口查询里存在大量「按楼栋 + 按状态 + 按姓名模糊」的拼装条件,手写动态 SQL 比方法名派生查询更直观。

2.1 依赖清单与版本组合

Maven 的pom.xml是第一个要看的地方,版本组合对不对,直接决定后面会不会踩到「jar 包冲突导致启动失败」这类无底洞。下面这段是这套工程里最核心的依赖声明:

<properties> <spring.version>5.3.20</spring.version> <mybatis.version>3.5.9</mybatis.version> <mybatis.spring.version>2.0.7</mybatis.spring.version> <mysql.version>8.0.28</mysql.version> <druid.version>1.2.8</druid.version> <jackson.version>2.13.2</jackson.version> <jsp.version>2.3.3</jsp.version> </properties> <dependencies> <!-- Spring 核心容器 + 事务 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- SpringMVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与整合包,版本必须成对 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.version}</version> </dependency> <!-- 驱动、连接池、分页 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.0</version> </dependency> <!-- Web 容器提供的,scope 必须是 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> </dependencies>

这段依赖有三个参数值得盯住。第一是mybatis-spring的版本,它必须和mybatis主版本对齐,2.0.x 系列对应 MyBatis 3.5.x,写错会直接在SqlSessionFactoryBean初始化时抛NoSuchMethodError,报错信息指向的却是一个看不出因果的类。第二是servlet-api和jsp-api的provided作用域,这两个包 Tomcat 自己有,打进 war 里就会和容器版本打架,典型症状是首页能开、一进 JSP 就 500。第三是 MySQL 驱动版本,8.x 驱动对应com.mysql.cj.jdbc.Driver和带时区参数的 URL,5.x 驱动对应com.mysql.jdbc.Driver,两套写法不能混用。

数据库连接信息不写死在 XML 里,抽到jdbc.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/community_population?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

serverTimezone这个参数是老生常谈但每年都有人翻车:8.x 驱动不写它,连接能建起来,但插入datetime字段时会整体偏移 8 小时,页面上显示的入住时间和实际登记时间对不上,排查半天以为是时区问题又以为是前端格式化问题。allowPublicKeyRetrieval=true则是 8.x 默认加密方式带来的附加条件,缺了它本地连库会报公钥检索失败。

2.2 目录结构:每一层放什么、谁来调谁

工程是标准 Maven Web 结构,包名按com.community.population起,往下按职责切分,不要按「模块」切——毕设工程规模不大,按职责切分找文件更快:

src/main/java/com/community/population ├── controller # 只做参数接收、调用 service、返回视图或 JSON ├── service # 业务接口 │ └── impl # 业务实现,事务注解打在这一层 ├── mapper # MyBatis 接口,方法名与 XML 的 id 一一对应 ├── entity # 与数据库表一一对应的 POJO ├── vo # 页面展示用的组合对象,比如带楼栋名的住户列表行 ├── dto # 接收前端查询条件的对象 ├── interceptor # 登录拦截、权限拦截 ├── util # MD5 加盐、日期工具、统一返回体 └── exception # 全局异常处理器 src/main/resources ├── jdbc.properties ├── spring-mybatis.xml # 数据源、SqlSessionFactory、事务管理器、mapper 扫描 ├── spring-mvc.xml # 注解驱动、视图解析器、拦截器、静态资源放行 ├── log4j.properties └── mapper # 所有 XML 映射文件,与接口同包名不同根 src/main/webapp ├── WEB-INF │ ├── web.xml │ └── views # JSP 页面,受 WEB-INF 保护不能直接访问 ├── static # bootstrap、jquery、layui 等前端资源 └── index.jsp

几个约定值得说清楚。entity里的类名和表名保持一一对应关系,比如Person对应t_person,字段用驼峰、列名用下划线,靠mapUnderscoreToCamelCase自动转换,省掉大量resultMap。vo是很多人会偷懒省掉的一层,但住户列表页要显示「楼栋号 + 单元 + 房号」,这三列分散在t_house和t_building里,如果直接把三个表的字段全塞进entity,实体类很快就变成一个几十字段的怪物,后面谁都不敢动。service层必须是接口加实现,事务注解打在实现类上,别打在接口上——Spring 的 AOP 代理默认基于接口,注解写在接口方法上有一部分场景不生效。

2.3 三个配置文件怎么串起来

SSM 的配置分散在web.xml、spring-mybatis.xml、spring-mvc.xml三处,初学者最容易糊涂的地方是:为什么有两个 Spring 容器?简单说,ContextLoaderListener起的是父容器,管 service、mapper、数据源、事务;DispatcherServlet起的是子容器,管 controller、视图解析、拦截器。子容器能拿到父容器的 bean,反过来不行。所以spring-mvc.xml里扫描 controller 时要排除 service,spring-mybatis.xml扫描 service 时要排除 controller,两边扫重了会导致事务失效——这个坑后面还会细说。

web.xml的关键片段:

<!-- 1. spring 父容器 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mybatis.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 2. 编码过滤器,必须放在所有过滤器最前面 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 3. SpringMVC 前端控制器 --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

<url-pattern>/</url-pattern>用的是「/」而不是「/」,前者只接管没匹配到其他 Servlet 的请求,静态资源还能走默认 Servlet;写成「/」会把.css、.js请求也吞进 DispatcherServlet,页面上就会出现样式全丢的经典场面。load-on-startup设为 1 是为了让容器启动时就初始化 DispatcherServlet,配置写错能第一时间在控制台看到,而不是等第一次访问才报错。

spring-mybatis.xml里负责数据源、工厂和事务:

<!-- 数据源:Druid,带连接池监控 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="2"/> <property name="maxActive" value="20"/> </bean> <!-- SqlSessionFactory:把 mapper xml 的位置告诉它 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.community.population.entity"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <!-- mapper 接口扫描,省掉逐个写 MapperFactoryBean --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.community.population.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> <!-- 事务管理器 + 注解驱动 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> <!-- 只扫 service,不扫 controller --> <context:component-scan base-package="com.community.population"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>

MapperScannerConfigurer里的sqlSessionFactoryBeanName用的是字符串而不是ref,原因是这个 Bean 是BeanDefinitionRegistryPostProcessor类型,执行时机比普通 Bean 早,用ref注入会触发「属性引用尚未初始化的 Bean」的循环依赖告警。mapUnderscoreToCamelCase开着,person_name就能自动映射到personName,少写一大堆resultMap。最后的exclude-filter是防事务失效的第一道保险,务必保留。

3. 核心模块拆解:一次人口登记从表单到入库的完整链路

人口登记是整个系统的中枢,其他模块——迁出、访客、报修、统计——都围着它转。这条链路的完整走向是:JSP 页面提交表单 → SpringMVC 参数绑定到 DTO → Controller 校验并调 Service → Service 开启事务,先查重(身份证是否已存在)、再写人口主表、再更新房屋入住状态、最后落一条迁入流水 → Mapper 执行 SQL → 事务提交 → 返回统一 JSON。链路不长,但每一环都有容易忽略的细节。

3.1 三张主表:楼栋、房屋、人口之间的从属关系

表结构决定了后面代码好不好写。这套系统用三张核心表撑起主数据,另外三张表记录动态行为:

-- 楼栋 CREATE TABLE t_building ( building_id INT PRIMARY KEY AUTO_INCREMENT, building_no VARCHAR(10) NOT NULL COMMENT '楼栋编号,如 3 号楼', unit_count INT DEFAULT 1 COMMENT '单元数', floor_count INT DEFAULT 6 COMMENT '楼层数', address VARCHAR(120) COMMENT '详细地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房屋,挂在楼栋下面 CREATE TABLE t_house ( house_id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, unit_no VARCHAR(10) NOT NULL COMMENT '单元号', room_no VARCHAR(10) NOT NULL COMMENT '房号', area DECIMAL(8,2) COMMENT '建筑面积', house_type VARCHAR(20) COMMENT '户型,如 两室一厅', status TINYINT DEFAULT 0 COMMENT '0 空置 1 自住 2 出租', KEY idx_building (building_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 人口,挂在房屋下面 CREATE TABLE t_person ( person_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, id_card VARCHAR(18) NOT NULL COMMENT '身份证号,业务唯一', gender TINYINT COMMENT '1 男 2 女', birth_date DATE, phone VARCHAR(20), house_id INT NOT NULL, relation VARCHAR(20) COMMENT '与户主关系:本人/配偶/子女/租客', resident_type TINYINT DEFAULT 0 COMMENT '0 常住 1 暂住 2 已迁出', come_date DATE COMMENT '入住日期', leave_date DATE COMMENT '迁出日期', remark VARCHAR(255), UNIQUE KEY uk_id_card (id_card), KEY idx_house (house_id), KEY idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三张表是严格的树形从属:楼栋 1:N 房屋,房屋 1:N 人口。这样设计的好处是「按楼栋统计常住人口」这类需求一条JOIN就能出结果,不用在人口表里冗余存楼栋号。代价是每次列表查询都要连三张表,所以idx_building、idx_house两个索引必须建,否则数据量上万后列表页加载会明显卡顿。id_card上加了唯一索引,这个设计有个副作用后面会讲——迁出再迁入的人怎么处理。

另外三张行为表各司其职:t_flow_record记录迁入迁出流水,字段是person_id / flow_type / from_addr / to_addr / happen_date / operator;t_visit_record记录访客,含来访人、被访人、身份证、进出时间;t_repair记录报修工单。流水表的存在很重要,人口主表只存「当前状态」,历史轨迹必须靠流水表回溯,否则统计「本月迁入多少人」就只能靠猜。

3.2 Mapper 层:动态 SQL 撑起多条件组合检索

人口管理页面顶上有五六个查询条件:姓名模糊、楼栋下拉、单元、居住状态、是否租客。用户可能只填其中一两个,也可能全填。这种场景用@Select注解拼 SQL 会写得很难看,正解是 XML 里的<where>+<if>:

<select id="selectPersonPage" resultType="com.community.population.vo.PersonVO"> SELECT p.person_id, p.name, p.id_card, p.gender, p.phone, p.resident_type, p.come_date, b.building_no, h.unit_no, h.room_no, h.house_type FROM t_person p INNER JOIN t_house h ON p.house_id = h.house_id INNER JOIN t_building b ON h.building_id = b.building_id <where> <if test="dto.name != null and dto.name != ''"> AND p.name LIKE CONCAT('%', #{dto.name}, '%') </if> <if test="dto.buildingId != null"> AND b.building_id = #{dto.buildingId} </if> <if test="dto.unitNo != null and dto.unitNo != ''"> AND h.unit_no = #{dto.unitNo} </if> <if test="dto.residentType != null"> AND p.resident_type = #{dto.residentType} </if> <if test="dto.relation != null and dto.relation != ''"> AND p.relation = #{dto.relation} </if> </where> ORDER BY b.building_no, h.unit_no, h.room_no </select>

这段 XML 有几个点要讲透。第一,<where>标签会自动处理开头多余的AND,也在所有条件都为空时不会生成WHERE关键字,比手写WHERE 1=1干净。第二,LIKE用的是CONCAT('%', #{...}, '%')而不是'%${...}%',#{}走预编译占位符,能防注入;${}是字符串拼接,用户输入一个单引号就能把 SQL 改掉,这是初学者最常写错的地方。第三,参数dto是查询条件对象,Mapper 接口里要写成selectPersonPage(@Param("dto") PersonQueryDTO dto),不写@Param时 MyBatis 会把对象属性直接暴露到根作用域,跟别的参数混在一起就会报「找不到属性」。第四,返回PersonVO而不是实体,因为building_no、unit_no、room_no这些字段实体里没有,靠resultType自动映射到 VO 的同名字段,字段名对不上就静默丢值,页面上表现为「楼栋号这一列永远是空的」。

分页不用自己算LIMIT,接 PageHelper:

@Override public PageInfo<PersonVO> pagePerson(PersonQueryDTO dto, int pageNum, int pageSize) { // startPage 必须紧贴查询语句,中间不能插入别的查询 PageHelper.startPage(pageNum, pageSize); List<PersonVO> list = personMapper.selectPersonPage(dto); return new PageInfo<>(list); }

PageHelper.startPage的原理是往当前线程的ThreadLocal里塞分页参数,MyBatis 执行下一条 SQL 时由拦截器改写加LIMIT,查完立刻清理。这意味着它只对「紧接着的第一条查询」生效。我就见过把startPage写在方法开头、中间先做了一次「查楼栋下拉列表」的情况,结果分页参数被那次查询消费掉,主列表查出来是全量数据,前端页码点一下整页刷不出来——这种问题没有异常、没有报错,纯靠肉眼排查,属于典型的黑匣子型 bug。

3.3 Service 层:迁入是写三张表,事务必须整体包住

登记一个新住户,表面上是往t_person插一行,实际上要做四件事:校验身份证是否重复、插入人口记录、把对应房屋状态改成「自住」或「出租」、写一条迁入流水。四步要么全成要么全不成,否则会出现「人登记上了但房屋还显示空置」这种脏数据,后面统计口径全乱。

@Service public class PersonServiceImpl implements PersonService { @Autowired private PersonMapper personMapper; @Autowired private HouseMapper houseMapper; @Autowired private FlowRecordMapper flowRecordMapper; @Override @Transactional(rollbackFor = Exception.class) public void register(PersonDTO dto, String operator) { // 1. 身份证查重:迁出状态的人允许重新登记,所以要先查全部 Person exist = personMapper.selectByIdCard(dto.getIdCard()); if (exist != null && exist.getResidentType() != 2) { throw new BizException("该身份证已登记,请勿重复录入"); } // 2. 写人口主表 Person person = new Person(); BeanUtils.copyProperties(dto, person); person.setResidentType(dto.getRelation().equals("租客") ? 1 : 0); person.setComeDate(new Date()); personMapper.insert(person); // 3. 更新房屋状态:租客置为出租,其余置为自住 houseMapper.updateStatus(dto.getHouseId(), person.getResidentType() == 1 ? 2 : 1); // 4. 落迁入流水 FlowRecord record = new FlowRecord(); record.setPersonId(person.getPersonId()); record.setFlowType(1); // 1 迁入 2 迁出 record.setToAddr(dto.getFullAddress()); record.setHappenDate(new Date()); record.setOperator(operator); flowRecordMapper.insert(record); } }

@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省。Spring 默认只对RuntimeException和Error回滚,抛受检异常时事务悄悄提交,数据就半截了。代码里抛的BizException继承自RuntimeException,属于能回滚的范畴,但如果哪天有人图省事改成受检异常,不加这个参数就会埋雷。

再者,这个方法里调的都是本类的mapper,没有「Service 调本类另一个 @Transactional 方法」的自调用问题。真实项目里如果写成this.updateHouseStatus()这种自调用,被调方法上的事务注解完全不生效,因为绕过了 AOP 代理。血泪经验是:事务方法拆到别的 Service 里,或者注入自己的代理对象,别图省事自调用。

3.4 Controller 层:参数绑定、校验与统一返回

Controller 只干三件事:接参数、调 Service、把结果包成统一结构返回。前端用的是 jQuery 发 AJAX + 页面局部刷新,所以返回 JSON 而不是 ModelAndView:

@Controller @RequestMapping("/person") public class PersonController { @Autowired private PersonService personService; @RequestMapping("/page") @ResponseBody public Result<PageInfo<PersonVO>> page(PersonQueryDTO dto, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { return Result.success(personService.pagePerson(dto, pageNum, pageSize)); } @RequestMapping(value = "/register", method = RequestMethod.POST) @ResponseBody public Result<Void> register(@Valid @RequestBody PersonDTO dto, HttpSession session) { // 操作人从 session 取,不信任前端传的值 SysUser loginUser = (SysUser) session.getAttribute("loginUser"); if (loginUser == null) { return Result.error("登录已失效,请重新登录"); } personService.register(dto, loginUser.getUsername()); return Result.success(); } }

@RequestBody要求前端Content-Type: application/json,配合 jQuery 的$.ajax({contentType: 'application/json'})。如果页面用的是表单默认提交(application/x-www-form-urlencoded),这里必须去掉@RequestBody,否则会报 415 不支持的媒体类型——这是前后端联调最常对不上的地方。

@Valid触发 JSR-303 校验,PersonDTO上标@NotBlank(message = "姓名不能为空")之类的注解,校验失败时统一由全局异常处理器拦截成 400 加提示文案,页面只需把提示弹出来。operator从 session 拿而不是让前端传,是个安全习惯:操作人字段如果可由前端指定,日志审计就形同虚设。

Result是一个极简的统一返回体,泛型加三个字段:

public class Result<T> { private Integer code; // 200 成功,500 业务异常,401 未登录 private String msg; private T data; public static <T> Result<T> success() { ... } public static <T> Result<T> success(T data) { ... } public static <T> Result<T> error(String msg) { ... } }

前端拿code判断,200 走正常渲染,401 直接跳登录页,500 弹msg。这套约定看着土,但比每个接口返回不同结构、前端写十几个 if 判断要省心得多。

4. 权限、分页与统计:管理端那几个绕不开的点

管理系统绕不过在「谁能看什么、谁能改什么」这件事上做控制。小区人口数据涉及身份证、电话这类敏感字段,权限设计不能只做到「登录了就能看」,要细分到角色和操作。这一块用最经典的 RBAC 模型落地,配合拦截器和 PageHelper 完成管理端的骨架。

4.1 登录拦截器:把未登录请求挡在业务之前

登录态存在HttpSession里,拦截器统一判断,别在每个 Controller 里重复写session.getAttribute:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 静态资源和登录接口放行 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/static/")) { return true; } Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // AJAX 请求和页面请求走不同响应 if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"登录已失效\"}"); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } return false; } return true; } }

返回false表示中断请求链。这里对 AJAX 和页面请求做了分流处理:AJAX 请求如果也重定向到登录页,前端会拿到一整段 HTML 塞进某个 div 里,页面看起来像坏了,用户根本不知道是掉登录。判断依据是 jQuery 默认带的X-Requested-With头,如果前端用了别的请求库可能没有这个头,要相应调整。

拦截器得在spring-mvc.xml里注册:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.community.population.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors> <!-- 静态资源必须放行,否则 CSS/JS 全部 404 --> <mvc:resources mapping="/static/**" location="/static/"/> <mvc:annotation-driven/>

<mvc:annotation-driven/>不能落下,它负责注册RequestMappingHandlerMapping和 JSON 消息转换器,少了它整个工程的@RequestMapping都不生效,访问任何路径都是 404。

4.2 RBAC:角色、菜单、用户三层绑定

系统里角色通常就四个:管理员、社区干事、物业、只读访客。权限模型用五张表:sys_user、sys_role、sys_menu、sys_role_menu、sys_user_role。管理员能进所有菜单、能删改;社区干事能登记和迁出,不能删;物业只能看报修和访客;只读访客只能查,页面上的「新增」按钮直接不渲染。

菜单级控制在登录时算好:用户登录 → 查角色 → 查角色关联菜单 → 把菜单树塞进 session → 左侧导航循环渲染。按钮级控制更简单粗暴:JSP 里用<c:if test="${sessionScope.menus.contains('person:add')}">包住按钮,同时后端接口上也加一层校验,不能只靠前端藏按钮——前端藏了只是看不见,接口地址还是能直接调。

后端校验写在一个自定义注解加拦截器里:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); // 权限编码,如 person:delete }

拦截器里拿到HandlerMethod,反射读注解上的编码,再和 session 里的权限集合比对,不匹配直接返回 403。这套做法比引 Shiro、Spring Security 轻得多,毕设规模完全够用,代码量也就一百来行。

4.3 统计查询:按楼栋聚合常住与暂住人数

首页看板要展示「各楼栋常住人口 / 暂住人口 / 空置房屋」三类数字,用一条聚合 SQL 一次查出来,别在前端循环调接口:

<select id="countByBuilding" resultType="com.community.population.vo.BuildingStatVO"> SELECT b.building_id, b.building_no, SUM(CASE WHEN p.resident_type = 0 THEN 1 ELSE 0 END) AS residentCount, SUM(CASE WHEN p.resident_type = 1 THEN 1 ELSE 0 END) AS tempCount, SUM(CASE WHEN h.status = 0 THEN 1 ELSE 0 END) AS emptyHouseCount FROM t_building b LEFT JOIN t_house h ON h.building_id = b.building_id LEFT JOIN t_person p ON p.house_id = h.house_id GROUP BY b.building_id, b.building_no ORDER BY b.building_no </select>

这里必须用LEFT JOIN而不是INNER JOIN,否则一栋刚建好、还没录房屋的楼会直接从结果里消失,看板上就少一块。SUM(CASE WHEN ...)是行转列的常用写法,注意「空置房屋数」如果和人口表一起 join,一个房屋有多个人时会重复计数,正确顺序是先按房屋去重再统计,或者把房屋统计单独拆一条 SQL。这类数字对不上是统计模块最常见的翻车点,页面显示「3 号楼空置 12 套,总房屋 10 套」的时候,基本就是这个 join 写重了。

5. 避坑与排查:启动失败、乱码、404 的定位顺序

这套工程如果按上面配置走,大概率能一次跑起来。但实际交付里出问题的点很集中,下面五条是从多个版本里反复出现的,按「现象 → 原因 → 解决」记下来,遇到问题照着顺序排。

5.1 启动报 ClassNotFoundException: ContextLoaderListener

现象:Tomcat 启动日志里刷出一长串ClassNotFoundException,先是ContextLoaderListener,然后整站 404。

原因:spring-web相关 jar 没进WEB-INF/lib。Maven 工程里常见两种触发方式,一是spring-web依赖被写成provided,二是 IDEA 里 artifact 没重新构建,只改了 pom 没同步到部署包。

解决:先看target/项目名/WEB-INF/lib下有没有spring-web-5.3.20.jar,没有就在 Project Structure → Artifacts 里把 Maven 依赖重新加进去,或者命令行跑mvn clean package后直接部署 war。改完 pom 一定要 clean,热部署不会自动带新 jar。

5.2 表单提交中文变问号或乱码

现象:新增住户时,姓名「张三」入库变成??,或者查询条件带中文时永远查不到数据。

原因:三个环节都可能没设编码——请求体编码、数据库连接编码、表字符集。CharacterEncodingFilter只解决第一个,jdbc.url里没写characterEncoding=utf8解决不了第二个,建表时用了latin1解决不了第三个。

解决:按顺序核三处。web.xml里过滤器forceEncoding设为true;jdbc.properties的 URL 带useUnicode=true&characterEncoding=utf8;建库语句写DEFAULT CHARSET=utf8mb4。三处都对了还乱码,就去查 Tomcat 的server.xml里 Connector 有没有配URIEncoding="UTF-8",老版本 Tomcat 默认不是 UTF-8,GET 请求参数会先乱在容器这一层。

5.3 页面样式全丢、JS 404

现象:JSP 能打开,但页面是纯文本排版,浏览器控制台一片 404,请求路径形如/person/static/css/bootstrap.css。

原因:静态资源被 DispatcherServlet 接管了,或者 JSP 里写的是相对路径static/css/...,而当前页面 URL 是/person/page,相对路径一拼就错位。

解决:两步。spring-mvc.xml里加<mvc:resources mapping="/static/**" location="/static/"/>放行;JSP 里统一用${pageContext.request.contextPath}/static/css/bootstrap.css写绝对路径。前者解决「进不来」,后者解决「找错门」,只做其中一个都还是会 404。

5.4 事务不生效,插入一半数据留在库里

现象:登记住户时故意让流水表插入失败,结果人口表那条记录还在,房屋状态也改了,只有流水没写进去。

原因:三种可能。一是@Transactional打在了 Service 接口上而不是实现类上;二是spring-mvc.xml和spring-mybatis.xml都扫了 service 包,子容器里生成了一个没有事务代理的副本;三是自调用,this.xxx()绕过了代理。

解决:注解挪到实现类方法上;检查两个配置文件的component-scan是否用exclude-filter互相排除;自调用改成注入自身代理或拆分到独立 Service。验证方式很简单,在事务方法里手动throw new RuntimeException(),看数据库有没有回滚,别靠读代码判断。

5.5 时间字段整体差 8 小时

现象:页面登记的入住日期是2024-05-20 10:00:00,数据库里存的是2024-05-20 02:00:00,或者反过来。

原因:MySQL 8 驱动默认按 UTC 解析时间,JVM 时区和数据库时区不一致时就会平移。

解决:JDBC URL 加serverTimezone=Asia/Shanghai;实体类时间字段统一用java.util.Date配@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),或者干脆用LocalDateTime并在mybatis-config里注册LocalDateTimeTypeHandler。前后端都统一成yyyy-MM-dd HH:mm:ss字符串传输,能省掉大量格式转换的麻烦。

6. 二次开发:加一个「流动人口上报」模块的完整改法

毕设交上去之后,最常被问的就是「能不能再加个功能」。以「流动人口上报」为例,走一遍改造流程,能看清这套分层结构该怎么扩展。

先改库。新建上报表,字段包括上报人、身份证、原籍、现住房屋、来小区日期、预计离开日期、审核状态:

CREATE TABLE t_float_report ( report_id INT PRIMARY KEY AUTO_INCREMENT, reporter VARCHAR(30) NOT NULL COMMENT '上报人姓名', id_card VARCHAR(18) NOT NULL, origin_addr VARCHAR(120) COMMENT '户籍地址', house_id INT NOT NULL, come_date DATE, expect_leave DATE, audit_status TINYINT DEFAULT 0 COMMENT '0 待审 1 通过 2 驳回', audit_remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_house (house_id), KEY idx_status (audit_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

然后是四件套:FloatReport实体、FloatReportMapper接口、mapper/FloatReportMapper.xml、FloatReportService及实现、FloatReportController。照着Person那一套抄,改动集中在两处业务逻辑。一是上报时校验house_id是否存在、该房屋是否已有人登记同身份证号的常住记录,避免重复上报。二是审核通过时不能只改状态,要把数据真正写进t_person并落一条迁入流水,所以审核方法必须带事务:

@Override @Transactional(rollbackFor = Exception.class) public void audit(Integer reportId, Integer status, String remark, String operator) { FloatReport report = floatReportMapper.selectById(reportId); if (report == null || report.getAuditStatus() != 0) { throw new BizException("上报记录不存在或已审核"); } floatReportMapper.updateAudit(reportId, status, remark); if (status == 1) { // 审核通过才转正,走与手工登记相同的人口入库逻辑 PersonDTO dto = new PersonDTO(); dto.setName(report.getReporter()); dto.setIdCard(report.getIdCard()); dto.setHouseId(report.getHouseId()); dto.setRelation("租客"); personService.register(dto, operator); } }

关键在于「审核通过」这一步复用了personService.register,而不是再写一遍插入逻辑。复用的好处是校验规则和流水记录只有一份,坏处是如果哪天登记逻辑变了,上报审核会跟着变——这恰恰是想要的,两条入口产出的数据必须完全一致,否则统计又对不上。

前端改动最小:在左侧菜单加一个「流动人口上报」,列表页复用人口列表的表格样式,把「审核」按钮做成弹出层,填审核意见提交。菜单配置写进sys_menu插一条记录,再给社区干事角色在sys_role_menu里授权,按钮才会显示出来。验证方法也简单:用只读账号登录,确认看不到上报菜单;用社区干事登录上报一条,换管理员审核通过,去人口列表按身份证搜,能查到且房屋状态已变成「出租」,再去流水表看有没有对应迁入记录——三步全对,模块就算接好了。

从那以后我每次给这类工程加模块,都强制走一遍「建表 → 实体 → Mapper → Service → Controller → 菜单授权 → 双角色验证」这条固定流程,一步不跳。跳过菜单授权,功能对但看不见;跳过双角色验证,权限漏洞就埋进去了。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 5:28:25

Python古诗词接龙:检索、打分与N-gram兜底实践

简介&#xff1a;这份项目包面向希望用Python完成自然语言处理实战的初中级开发者&#xff0c;围绕“诗歌接龙”场景整合文本分词、拼音转换、韵脚匹配与接龙算法等内容&#xff0c;可用于课程设计、练手项目或NLP流程入门。压缩包共17个文件&#xff0c;约5.86MB&#xff0c;包…

作者头像 李华
网站建设 2026/11/6 5:28:17

基于SharpPcap的C# ARP局域网设备发现与IP冲突检测

简介&#xff1a;面向使用 C# 进行局域网运维与网络监测的开发者和网管人员&#xff0c;这份资源围绕抓取 ARP 数据包来识别在线网络设备&#xff0c;并整理出存在 IP 冲突的设备列表&#xff0c;适合需要实现设备发现、冲突排查或底层抓包入门的中级读者参考。压缩包共 36 个文…

作者头像 李华
网站建设 2026/11/6 5:26:54

实验室管理系统源码包启动联调与毕业设计验收避坑指南

简介&#xff1a;这套实验室管理系统面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生&#xff0c;也可供需要完整项目实战练习的开发者参考。项目采用Java语言与Spring Boot后端框架&#xff0c;数据库选用MySQL&#xff0c;开发工具为IntelliJ IDEA&#xff…

作者头像 李华
网站建设 2026/11/6 5:24:30

Docker 容器|Flink 消费 Kafka!自定义网络

接上一篇 bitnami/kafka 镜像踩坑&#xff01; 很多同学搭完 Kafka&#xff0c;启动 Flink 容器死活连不上&#x1f62d; 核心坑&#xff1a;容器之间不能写localhost&#xff01;&#xff01; Flink 和 Kafka 放在同一个 docker 自定义网络&#xff0c;直接用服务名通信&am…

作者头像 李华
网站建设 2026/11/6 5:23:24

【大数据毕设项目】基于MLP神经网络的居民高血压风险预测与可视化分析系统\基于机器学习的居民体检糖尿病及心血管疾病风险预测系统

文章目录 一、项目开发背景意义 二、项目开发技术 三、项目开发内容 四、项目展示 五、项目相关代码 六、最后 一、项目开发背景意义 系统围绕健康指标分析与风险预测两大核心目标&#xff0c;构建了涵盖数据管理、可视化分析与智能预测的综合服务平台。系统集成了多源异…

作者头像 李华
网站建设 2026/11/6 5:23:05

BI 报表使用率低的五个原因与排查清单

项目交付时一切正常&#xff0c;半年后系统却没人用了——这是 BI 实施里很常见的现象。多数不是模型或可视化的问题&#xff0c;而是分发和使用体验的问题。下面整理成一份排查清单&#xff0c;方便在上线前后自查。排查项 1&#xff1a;访问路径过长现象&#xff1a;业务同事…

作者头像 李华