ARTICLE DETAIL

资讯详情

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

SSM框架整合实战:小区人口管理系统开发与避坑指南

SSM框架整合实战:小区人口管理系统开发与避坑指南 简介一份基于SSMSpring/SpringMVC/MyBatis框架开发的小区人口管理系统毕业设计程序适合计算机相关专业学生用于毕业设计参考或课程设计。系统围绕小区住户档案、费用信息管理、出入登记、疫情黑名单等核心模块展开实现了SpringMVC请求分发、MyBatis数据库映射、Vue前端交互以及SQL表结构设计能够直观呈现SSM整合开发的全流程。压缩包共305个文件约1.04MB包含70个Java源码与70个编译后的class文件、67个Vue文件、29个XML配置、SQL脚本等按后端逻辑、前端页面和数据库脚本分类存放学习时可按模块快速定位。已有153人学习。附带的源代码和数据库均能直接运行配置环境说明可帮助快速搭建本地调试环境通过阅读关键模块的Controller与Entity代码还可以学习用户登录校验、费用数据关联查询、出入登记状态管理等实用技巧为后续功能扩展和毕业设计文档撰写提供参照。1. ssm小区人口管理系统为什么年年有人做还年年有人翻车每年毕业季计算机专业的选题清单里总有几个常青树ssm小区人口管理系统就是其中之一。它的定位很明确用Spring、Spring MVC、MyBatis这一套Java Web老牌组合做一个人口信息的后台管理系统覆盖楼栋管理、住户管理、人口信息的增删改查与统计展示。看起来平平无奇但真正动手做的人里至少有一半卡在框架整合阶段连登录页都跑不出来。这个选题的价值恰恰在这里它让你用最少的业务复杂度把ssm框架的三层架构、配置文件、拦截器、MyBatis映射这些核心技能完整走一遍既符合毕业设计对工作量的要求又能让答辩老师一眼看出你懂框架。适合想快速跑通一个完整项目、把SSM技术栈吃透的在校学生也适合需要用一套经典案例做内部培训的团队。2. 拆开ssm框架三个组件各自管什么整合的关键在哪里2.1 Spring管对象Spring MVC管请求MyBatis管数据ssm框架里三个组件的分工刚接触的人很容易混。Spring是容器负责所有业务对象的创建、依赖注入和事务控制Spring MVC是Web层框架负责接收HTTP请求、调用Controller、返回视图MyBatis是持久层框架负责把Java方法和SQL映射起来替你把JDBC那套重复代码收掉。三者各自独立但整合在一起时边界一旦划不清就会出问题。我一般是这样给项目划层的浏览器请求先进Spring MVC的DispatcherServletDispatcherServlet通过HandlerMapping找到对应的Controller方法Controller里注入的是Service接口Service里注入的是Mapper接口Mapper接口由MyBatis生成实现类最终操作数据库。Spring在整个过程中做胶水把Controller、Service、Mapper这些Bean统一装进容器事务边界划在Service层。这个分层方式在小区人口管理系统里的体现是人口信息的增删改查逻辑写在Service里Controller只做参数接收和视图跳转Mapper只做SQL映射。很多人翻车是因为把SQL写在Service里或者把业务判断堆在Controller里代码也能跑但后续调试和答辩讲架构时非常被动。2.2 项目目录结构一个能跑的SSM工程长什么样一个标准的ssm小区人口管理系统不依赖Maven也能搭但既然要做毕业设计我建议直接用Maven工程结构。它能把依赖管理和打包问题一起解决掉答辩时老师问“项目怎么构建的”你至少能接住话。ssm-community/ ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/community │ │ │ ├── controller │ │ │ ├── service │ │ │ │ └── impl │ │ │ ├── mapper │ │ │ ├── entity │ │ │ └── interceptor │ │ ├── resources │ │ │ ├── jdbc.properties │ │ │ ├── spring.xml │ │ │ ├── spring-mvc.xml │ │ │ └── mapper │ │ └── webapp │ │ ├── WEB-INF │ │ │ ├── web.xml │ │ │ └── views │ │ ├── static │ │ │ ├── css │ │ │ └── js │ │ └── index.jsp这个结构的核心逻辑是controller、service、mapper、entity四层对应ssm框架的四层职责resources/mapper下放MyBatis的XML映射文件webapp下放页面和静态资源。注意interceptor包登录拦截器放在这里后面第四部分会具体写。我见过有人把所有Java文件全塞在一个包里页面直接扔在webapp根目录程序也能跑但项目结构这一项在毕业设计评分表里占分不少而且后续你自己维护也会疯掉。目录结构不只是好看更多是让Spring的包扫描、MyBatis的Mapper扫描有明确的边界可循。2.3 三份核心配置文件的写法与参数SSM项目的配置文件是最大的拦路虎。配置文件写不对启动时要么报Bean找不到要么报404而且报错信息往往不够直白。整套项目有三份核心配置web.xml、spring.xml、spring-mvc.xml。先看web.xml它是整个Web应用的入口web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namespringMVC/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringMVC/servlet-name url-pattern//url-pattern /servlet-mapping /web-app这段配置里CharacterEncodingFilter要放在最前面且url-pattern必须是/*否则POST请求的中文乱码问题会让你怀疑人生。DispatcherServlet的url-pattern配成/表示接管所有请求这样Spring MVC才能处理页面跳转和接口请求。这里有个很多人踩过的坑url-pattern配成/*会连JSP的渲染也被DispatcherServlet拦截导致页面全是空白或下载文件这里必须用/。接下来看spring.xml它负责Spring容器的初始化beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:contexthttp://www.springframework.org/schema/context xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd context:component-scan base-packagecom.community context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.community.entity/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.community.mapper/ /bean /beanscomponent-scan配的是Service、Mapper这些Bean的扫描范围要点是排除Controller注解因为Controller交给Spring MVC那套配置管理。数据源我惯用Druid它在本地调试时能在页面直接看到SQL执行情况比默认的连接池更直观。SqlSessionFactoryBean是MyBatis和Spring整合的关键mapperLocations指向XML映射文件的位置typeAliasesPackage让你在XML里写resultType时可以直接用resident而不用写全限定类名。最后是spring-mvc.xmlbeans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:contexthttp://www.springframework.org/schema/context xmlns:mvchttp://www.springframework.org/schema/mvc xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd context:component-scan base-packagecom.community.controller/ mvc:annotation-driven/ mvc:resources mapping/static/** location/static// bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.community.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors /beans这段配置里mvc:resources必须配否则浏览器请求CSS和JS会被DispatcherServlet拦截页面样式全丢这是SSM新手最经典的翻车点之一。InternalResourceViewResolver的prefix配合/WEB-INF/views/意味着Controller里return字符串后会自动拼接成/WEB-INF/views/xxx.jsp。拦截器的exclude-mapping里的/static/**尤其重要不然静态资源会被拦截器挡在登录页外面。三份配置文件的依赖关系是web.xml先启动DispatcherServlet加载spring-mvc.xml同时spring.xml由ContextLoaderListener加载两份Spring容器共享但互不干扰。这里如果出现“Bean找不到”的报错优先检查两件事spring.xml里是否把Controller排除了spring-mvc.xml里是否只扫描了controller包。3. 人口数据怎么建模表结构设计与SQL脚本3.1 五张核心表从楼栋到人口的层级关系小区人口管理系统的数据模型核心是“楼栋—住户—人口”的层级关系。最合理的设计是拆成五张表管理员表、楼栋表、住户表、人口表、登录日志表。不用再多了毕业设计不是企业级项目表拆得太碎反而不好讲清楚。五个表的字段设计要遵循一个原则人口表只存人的属性和与住户表的关联不存楼栋信息住户表只存房屋信息和户主关联楼栋表只存楼栋本身的属性。这样查询时通过表的JOIN就能拿到完整信息而不需要冗余字段。我见过有人把所有信息塞进一张表字段多达三十几个美其名曰“简化设计”。实际写起来每次查询都要带上一堆无关字段统计SQL根本没法写然后又在代码里做各种判断来兜底。好的表结构应该让SQL越写越简单而不是越写越绕。五张表的关系用一句话描述登录日志挂管理员楼栋下挂住户住户下挂人口人口里有一个字段标识是否户主。3.2 建表SQL与初始化数据下面是完整的建表脚本直接放到MySQL里执行即可CREATE DATABASE IF NOT EXISTS community DEFAULT CHARSET utf8mb4; USE community; CREATE TABLE admin_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ) ENGINEInnoDB; CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT, building_no VARCHAR(20) NOT NULL UNIQUE, building_name VARCHAR(50), floors INT DEFAULT 6, units INT DEFAULT 2 ) ENGINEInnoDB; CREATE TABLE household ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, unit_no VARCHAR(10), room_no VARCHAR(10) NOT NULL, owner_id INT, tel VARCHAR(20), move_in_date DATE, CONSTRAINT fk_household_building FOREIGN KEY (building_id) REFERENCES building(id) ) ENGINEInnoDB; CREATE TABLE resident ( id INT PRIMARY KEY AUTO_INCREMENT, household_id INT NOT NULL, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL, gender TINYINT DEFAULT 0, birthday DATE, relation_owner TINYINT DEFAULT 0, household_type VARCHAR(20) DEFAULT 常住, is_owner TINYINT DEFAULT 0, CONSTRAINT fk_resident_household FOREIGN KEY (household_id) REFERENCES household(id) ) ENGINEInnoDB; CREATE TABLE login_log ( id INT PRIMARY KEY AUTO_INCREMENT, admin_id INT, login_time DATETIME, ip VARCHAR(50), CONSTRAINT fk_log_admin FOREIGN KEY (admin_id) REFERENCES admin_user(id) ) ENGINEInnoDB; INSERT INTO admin_user (username, password, real_name) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 系统管理员);这段SQL的关键设计有三处。resident表里is_owner字段用来标记户主owner_id放在household表里形成人的归属关系避免查询时还要去人口表里找户主。id_card设定为18位定长为的是后面写身份证校验逻辑时有明确边界。外键约束一定不要省虽然有人说会影响性能但在这个系统里它能在你删除楼栋时直接挡住错误操作比在业务层写一堆判断可靠得多。管理员初始密码这里存的是“123456”的MD5值。答辩现场演示时你要当着老师的面登录所以密码不要设得太复杂方便快速进入系统。需要用明文密码做演示的话把password字段的密文改成明文也行但建议不要这么干答辩老师往往会注意到这种安全性细节。3.3 关联查询与统计查询的SQL写法建好表之后最重要的就是查询。后台列表页需要展示“楼栋号—单元—房号—户主姓名—人口数”这样的组合信息SQL的JOIN关系必须提前理清SELECT h.id, b.building_no, h.unit_no, h.room_no, r.name AS owner_name, r.tel, (SELECT COUNT(*) FROM resident r2 WHERE r2.household_id h.id) AS person_count FROM household h JOIN building b ON h.building_id b.id LEFT JOIN resident r ON h.owner_id r.id ORDER BY b.building_no, h.unit_no, h.room_no;这条SQL里person_count用了子查询来统计每个住户下的人口数注意这个是Core逻辑不要在Java层面用“先查住户再遍历数人口”的方式实现那样会引发经典的N1查询问题列表稍微长一点页面就会卡。ORDER BY按楼栋号、单元号、房号排序刚好对应小区门牌的视觉顺序演示时观感很好。另一个必写的统计查询是各楼栋人口分布SELECT b.building_no, COUNT(DISTINCT h.id) AS household_count, COUNT(r.id) AS resident_count FROM building b LEFT JOIN household h ON b.id h.building_id LEFT JOIN resident r ON h.id r.household_id GROUP BY b.id, b.building_no ORDER BY b.building_no;这里用COUNT(DISTINCT h.id)而不是COUNT(h.id)是因为两个LEFT JOIN之后会产生笛卡尔积一个人口会对应一条住户记录直接COUNT会把人数算错。这是统计类SQL最常见的错法建议写完用数据验证一下。统计模块还可以加一个户籍类型分布SELECT household_type, COUNT(*) AS cnt FROM resident GROUP BY household_type;表格展示或者图表展示都可以。如果做图表把这三条SQL的结果封装成JSON往前端传就行ECharts的饼图和柱状图直接就能接上。4. 从零跑通核心功能后台登录与人口信息管理的完整代码4.1 登录模块Session校验和拦截器登录模块是SSM项目里最体现框架整合能力的部分。用户提交表单Controller校验用户名密码成功后写入Session然后所有页面访问都通过拦截器校验Session。这里有一个设计决策密码校验放在Service层做Controller只做参数传递和结果判断。先写Mapper层接口package com.community.mapper; public interface AdminUserMapper { AdminUser findByUsername(String username); }对应的XML映射文件放在resources/mapper/AdminUserMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.community.mapper.AdminUserMapper select idfindByUsername parameterTypestring resultTypeadminUser SELECT * FROM admin_user WHERE username #{username} /select /mapper我这里resultType直接写adminUser而不是全限定类名是因为spring.xml里配置了typeAliasesPackage这能少写很多冗长代码。#{username}是预编译参数MyBatis会把它转成?占位符不用手动拼接SQL这也是MyBatis存在的意义之一。Service层做密码校验逻辑package com.community.service; public interface AdminUserService { boolean login(String username, String password); } Service public class AdminUserServiceImpl implements AdminUserService { private final AdminUserMapper adminUserMapper; Autowired public AdminUserServiceImpl(AdminUserMapper adminUserMapper) { this.adminUserMapper adminUserMapper; } Override public boolean login(String username, String password) { AdminUser user adminUserMapper.findByUsername(username); if (user ! null) { String md5 DigestUtils.md5DigestAsHex(password.getBytes()); return md5.equalsIgnoreCase(user.getPassword()); } return false; } }这里有个细节用构造器注入而不是字段注入Spring容器初始化时就能发现依赖缺失而不是运行到一半才报NullPointerException。密码比对用MD5本地项目足够。DigestUtils是Spring自带的工具类比你自己写MessageDigest的try-catch简洁得多少一段异常处理的样板代码。然后是拦截器package com.community.interceptor; public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /loginPage); return false; } return true; } }拦截器本身的逻辑极其简单复杂点在spring-mvc.xml里的放行路径。exclude-mapping里必须包含/loginPage和/login不然用户连登录页面都进不去自己把自己锁在门外的事我见过不止一次。4.2 人口信息的增删改查从Controller到Mapper人口信息增删改查是整个系统的核心流程。我以一个新增人口为例把整条链路串起来。先写Controllerpackage com.community.controller; Controller RequestMapping(/resident) public class ResidentController { private final ResidentService residentService; Autowired public ResidentController(ResidentService residentService) { this.residentService residentService; } PostMapping(/add) public String addResident(RequestParam Integer householdId, RequestParam String name, RequestParam String idCard, RequestParam(defaultValue 0) Integer gender, RequestParam(defaultValue 0) Integer isOwner, Model model) { boolean ok residentService.addResident(householdId, name, idCard, gender, isOwner); if (ok) { return redirect:/resident/list?householdId householdId; } model.addAttribute(errorMsg, 添加失败请检查身份证是否重复); return resident/add; } }这个Controller只做参数绑定和结果跳转。redirect:前缀表示重定向刷新页面不会重复提交表单这是POST请求必须的处理习惯。defaultValue保证前端漏传参数时程序不直接500。Service层负责业务约束Service public class ResidentServiceImpl implements ResidentService { private final ResidentMapper residentMapper; Autowired public ResidentServiceImpl(ResidentMapper residentMapper) { this.residentMapper residentMapper; } Override public boolean addResident(Integer householdId, String name, String idCard, Integer gender, Integer isOwner) { Resident resident new Resident(); resident.setHouseholdId(householdId); resident.setName(name); resident.setIdCard(idCard); resident.setGender(gender); resident.setIsOwner(isOwner); resident.setBirthday(parseBirthdayFromIdCard(idCard)); return residentMapper.insert(resident) 0; } }这里有个可以直接抄的细节parseBirthdayFromIdCard从18位身份证里截取第7到14位作为出生日期格式如19900101解析成yyyy-MM-dd存进birthday字段。这样用户不需要单独填出生日期表单少一个字段录入效率高答辩时还能解释你做了字段派生设计。MyBatis的insert语句insert idinsert parameterTyperesident useGeneratedKeystrue keyPropertyid INSERT INTO resident (household_id, name, id_card, gender, birthday, household_type, is_owner) VALUES (#{householdId}, #{name}, #{idCard}, #{gender}, #{birthday}, 常住, #{isOwner}) /insertuseGeneratedKeystrue配合keyPropertyid让MySQL自增主键回写到Java对象的id属性后续操作不用再查一次数据库。household_type默认值直接写死在SQL里比在Java里set更直观减少Service层的样板代码。列表查询Controller这样写GetMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Integer buildingId, Model model) { PageHelper.startPage(pageNum, pageSize); ListResidentVO list residentMapper.selectList(keyword, buildingId); PageInfoResidentVO pageInfo new PageInfo(list); model.addAttribute(page, pageInfo); return resident/list; }PageHelper.startPage要放在Mapper方法调用前一行中间不能隔着其他数据库操作否则分页不生效这是PageHelper的硬性规定。后面会专门讲这个问题。列表查询在XML里用动态SQL实现select idselectList resultTypecom.community.entity.ResidentVO SELECT r.*, h.building_id, b.building_no, h.unit_no, h.room_no FROM resident r JOIN household h ON r.household_id h.id JOIN building b ON h.building_id b.id where if testkeyword ! null and keyword ! AND (r.name LIKE CONCAT(%, #{keyword}, %) OR r.id_card LIKE CONCAT(%, #{keyword}, %)) /if if testbuildingId ! null AND h.building_id #{buildingId} /if /where ORDER BY b.building_no, h.unit_no, h.room_no, r.id /select动态SQL里where标签很关键它会自动去掉第一个AND这样条件加不加都不会出现SQL语法错误。CONCAT(%, #{keyword}, %)这种写法是刻意避开字符串拼接注入风险虽然#{keyword}本身是预编译但写成这样更符合规范。4.3 分页与条件查询PageHelper插件的两个必配参数SSM项目做分页业界基本都用PageHelper。它底层通过MyBatis拦截器改写SQL自动生成LIMIT语句你不需要手写分页条件。但有两个参数必须配不配就会出问题。在spring.xml的SqlSessionFactoryBean中配置插件property nameplugins array bean classcom.github.pagehelper.PageInterceptor property nameproperties value helperDialectmysql reasonabletrue supportMethodsArgumentstrue /value /property /bean /array /property第一个必配参数是helperDialectmysql它告诉PageHelper你用的是MySQL方言这样生成的LIMIT语句格式才对。第二个必配参数是reasonabletrue它的作用是当你请求第0页时自动修正为第1页请求超过总页数时自动修正为最后一页。不配这个参数的话用户手动改页面URL里的页码参数系统直接报错。supportMethodsArgumentstrue允许从方法的参数中直接读取分页参数配合Spring MVC的RequestParam绑定可以少写很多代码。如果启动时看不到分页效果检查一下pom里PageHelper版本和MyBatis版本是否匹配。PageHelper 5.x要求MyBatis 3.5以上版本不匹配时会静默失效不报错但也不分页这个坑非常隐蔽。5. 避坑记录SSM项目最常见的5个翻车现场5.1 Mapper接口报找不到Mapper与MapperScan现象启动Spring容器时报org.springframework.beans.factory.NoSuchBeanDefinitionException提示找不到AdminUserMapper这个Bean但接口和XML文件都写得没错。原因MyBatis的Mapper接口和普通接口不一样它没有实现类Spring的包扫描默认无法为接口创建代理对象。如果既没有在接口上加Mapper注解也没有在配置类或spring.xml里加MapperScannerConfigurerSpring就不知道这个接口是MyBatis的Mapper。解决两种做法选一个。我在spring.xml里已经配了MapperScannerConfigurer指定basePackage为com.community.mapper这是全局做法一劳永逸。如果用的是Spring Boot或者更零散的配置方式也可以在每个Mapper接口上加Mapper注解。但毕业设计的SSM项目我建议只用MapperScannerConfigurer因为接口数量少不需要在每个文件上重复标注。5.2 页面样式全丢Spring MVC静态资源拦截现象登录页能打开但CSS和JS全部加载失败浏览器控制台一堆Failed to load resource页面光秃秃的像纯文本。原因DispatcherServlet的url-pattern在web.xml里配成/后所有请求都会经过它。Spring MVC没有静态资源处理器时CSS、JS这些请求会被路由到Controller匹配匹配不到就返回404。解决在spring-mvc.xml里加mvc:resources mapping/static/** location/static//然后再在拦截器配置里加一行mvc:exclude-mapping path/static/**/。前一个保证DispatcherServlet放行静态资源后一个保证登录拦截器也放行否则静态资源被拦到了登录页面样式照样加载不出来。5.3 查询结果都是null数据库列名与Java属性驼峰映射现象列表页面能打开但每条数据的buildingNo、roomNo全是null只有id有值。SQL在Navicat里执行正常但通过MyBatis查出来就缺字段。原因数据库列名是下划线风格building_noJava属性是驼峰风格buildingNo。MyBatis默认不做自动映射转换列名和属性对不上就返回null。这个问题最容易出现在JOIN查询的VO上实体表CRUD反而少出现因为列名被IDE生成器处理过。解决在spring.xml的SqlSessionFactoryBean里配置MyBatis的mapUnderscoreToCamelCase为true这段配置在第二部分已经写过。开了这个开关后building_no会自动映射到buildingNoid_card自动映射到idCard。注意这个开关只对下划线转驼峰有效列名全大写的照样会出问题所以建表时养成全部小写加下划线的习惯。5.4 中文乱码连接URL和过滤器一个都不能少现象新增人口时输入中文姓名保存后数据库里显示乱码或者查询界面上传中文条件查不出结果。原因乱码有两个入口。一是数据库连接URL没指定编码MySQL连接时用了默认的latin1字符集二是Spring MVC没配字符编码过滤器POST请求体里的中文参数无法被正确解码。解决两处都要改。jdbc.properties里的连接URL加上useUnicodetruecharacterEncodingUTF-8同时web.xml里配上CharacterEncodingFilter。我见过有人只改了连接URL结果表单提交还是乱码以为是自己代码的问题其实Spring MVC的POST参数解码和JDBC连接是两条独立的链路缺一个都不行。另外MySQL建库时用utf8mb4而不是utf8原因不多说直接照做就行。5.5 删除住户提示外键冲突先删子表再删主表现象在住户管理里删除一个不满意的住户记录程序报错Cannot delete or update a parent row: a foreign key constraint fails。原因住户表household被人口表resident的外键引用数据库层面不允许直接删除有子记录的主表数据。按业务逻辑来说这也是对的一个住着人的房子不该被直接删掉。解决删除操作要分两步。先删除该住户名下所有人口记录再删除住户本身。这个顺序放在Service层的方法里用Transactional注解包起来保证两步操作原子性。删除楼栋也是同样逻辑先删人口再删住户最后删楼栋。如果业务允许“住户搬空”的状态可以先把人口记录移到历史表再删但毕业设计不需要这么复杂按层级顺序删就是合理设计。6. 从能跑通到能答辩演示顺序、测试数据和验证清单系统跑通只是第一步毕业设计答辩是另一个战场。我见过不少项目功能完整但演示时翻车的情况根源是没提前设计演示路径。演示顺序我建议固定为登录 → 楼栋管理→ 住户管理 → 人口信息增删改查 → 统计报表 → 安全退出。这六步刚好覆盖你的六个Controller和全部核心表每步之间逻辑连贯老师也能顺着你的节奏理解系统结构。测试数据要往真实里做。楼栋至少建三栋每栋的单元和房号按真实小区的习惯填住户数据每个楼栋放七八户人口数据每户两三个人。身份证号要用满足校验规则的虚拟号码性别、出生日期要跟身份证号本身的时间戳对应上。答辩老师会随机挑一条数据看详情页如果数据自相矛盾印象分会掉不少。班级信息、籍贯这些字段的测试值建议直接找一个真实的城市和街道名来模拟整个演示过程会显得系统是真在跑而不是背课文。验证清单我一般建议放在最后三天逐项跑第一空密码、错误密码登录能否被正确拦截第二分页跳转和搜索条件组合后数据是否一致第三新增、编辑、删除一轮后列表页统计数字是否同步更新第四连续点角色权限以外的页面路径是否被拦截器挡住。这四项没问题系统在演示环节基本稳了。反正在我自己带过的项目组里能把演示顺序背熟、测试数据嚼烂的组员答辩成绩普遍比代码多炫但临场卡壳的那些人高一个档次。希望这份笔记能把你的毕业设计从“能跑”带到“能讲清楚”祝你一次通过。本文还有配套的精品资源点击获取
返回列表