简介:这是一份基于SSM框架与MySQL的社区居家养老服务网站完整项目源码及数据库脚本,面向计算机相关专业毕业生以及需要Java Web整合练习的开发者。项目涵盖前端页面、后端业务逻辑、数据库表结构等完整闭环,源码已通过本地编译运行验证,评审分达到95分以上,难度适中,适合作为毕业设计参考或二次开发基础。压缩包共1130个文件,大小约18.89MB。其中html、css、js构成页面结构与交互效果,java、jsp负责后端处理与动态页面,png等图片支撑界面展示,另有jar依赖包、xml配置文件及sql数据库脚本,便于直接导入项目并恢复数据。目前已有116人学习下载。借助这份资源,可掌握SSM框架整合思路、社区养老服务功能模块设计方法,以及数据库脚本导入和调试流程。对准备毕业设计答辩或快速搭建同类管理网站的同学,参考价值较高。
1. 社区居家养老服务网站:SSM 毕设里性价比最高的那一档
每年毕业设计一到四五月,Java 方向被问得最多的就是「有没有能跑、能讲、能答辩的 SSM 项目」。这个社区居家养老服务网站就是典型的那类答案:Spring + SpringMVC + MyBatis 的 SSM 组合,配 MySQL 存数据,前端用 EasyUI 做后台管理界面、Bootstrap 做展示页,源码压缩包里带全套数据库脚本。评审能到 95 分,说明功能完整度和代码规范都在平均线以上,拿去做本科毕设或者课程设计,改动成本很低。适合两类人:一类是 Java 基础一般、想找个完整项目兜底的同学,另一类是已经能写 SSM 但不想从零搭前端和权限的同学。这篇笔记按我拆项目的习惯,把技术栈、数据库脚本、前端页面和部署流程一条线捋清楚,并标出最容易卡住的地方。
2. SSM 养老项目的地基:建表脚本与核心模块拆解
2.1 数据模型的设计意图:五张核心表把业务串起来
养老服务平台和电商网站不一样,它的核心不是商品,而是「人 + 服务」。老人、护工、服务项目、订单、健康档案,这五类数据是整个系统的骨架。我拆这个项目时先看的不是代码,而是 SQL 脚本,因为表结构决定了业务边界。
常见的设计是这几张表:
elder_info:老人档案表,主键elder_id,字段包含姓名、年龄、身份证号、家属电话、住址、健康状况备注。service_item:服务项目表,比如助餐、助浴、家政、康复理疗。service_order:服务工单表,关联老人和服务项目,同时记录下单时间、预约时间、指派护工、状态。nurse_info或者sys_user:护工/管理员账号表,角色字段区分管理员和护工。health_record:健康档案表,定期体检记录,血糖血压等指标。
这五张表足够覆盖一个居家养老网站的全部流程:管理员维护服务项目,老人或家属下单服务,系统生成工单,护工接单并上门,服务完成后在健康档案里追加记录。我习惯先画这个闭环再看代码,否则很容易被前后端代码带着走。
数据库脚本一般是.sql文件,里面除了建表语句,还带几条初始数据。注意看sys_user表里的初始账号,默认密码通常经过 MD5 加密存储,这一点在 4.2 里单独列出。
2.2 从压缩包文件名反推系统结构:bootstrap.css 与 easyui.css 的分工
项目正文里列出过bootstrap.css、easyui.css这些文件名,这其实是很好的线索。一个 SSM 项目里同时出现这两套 CSS,大概率是「门户展示页用 Bootstrap,后台管理页用 EasyUI」的混搭架构。这不是乱用,而是当时很常见的低成本方案。
Bootstrap 管的是面向老人/家属的页面:首页轮播、服务项目展示、预约表单、公告列表,这些页面要求观感好、能自适应屏幕。EasyUI 管的是后台管理:管理员维护服务项目、查看工单、管理老人档案,这些页面重表格、重操作效率,EasyUI 的 datagrid 组件非常适合。
判断一个项目是否混搭,直接看webapp目录下的结构,通常会拆成admin和portal两块。这个项目的静态资源分类也是这个套路,Bootstrap 相关的 css/js 放在 portal 对应目录,easyui 系列统一放 admin 相关目录。如果你要把这套东西改成 vue 前端,至少后台表格这一块的替换成本是最高的。
2.3 MyBatis Mapper 与业务层的事务边界
SSM 项目里 MyBatis 的用法存在两种风格:一是 SQL 写在 Mapper XML 里,二是直接用注解。这个项目用的是 XML 文件维护 SQL,老派但稳定,毕业设计答辩时也更好讲,因为 SQL 逻辑清晰可查。
看 Mapper XML 时重点看三点:一是参数类型和 resultMap 是否对应,二是关联查询是单独写还是用association,三是动态 SQL 用的多不多。以服务工单查询为例,典型写法是:
<select id="selectOrderList" resultMap="OrderResultMap" parameterType="map"> SELECT o.order_id, o.elder_id, o.service_id, e.elder_name, e.elder_phone, s.service_name, o.status, o.order_time FROM service_order o LEFT JOIN elder_info e ON o.elder_id = e.elder_id LEFT JOIN service_item s ON o.service_id = s.service_id <where> <if test="status != null and status != ''"> AND o.status = #{status} </if> <if test="elderName != null and elderName != ''"> AND e.elder_name LIKE CONCAT('%', #{elderName}, '%') </if> </where> ORDER BY o.order_time DESC LIMIT #{start}, #{rows} </select>这段 SQL 在项目里很常见,它做三件事:三表关联查询、条件动态拼接、分页。注意#{start}和#{rows}对应 EasyUI datagrid 传来的分页参数,这块在第三章展开。
事务边界放在 Service 层,下单时要写订单表、改服务项目排期、给护工发任务,这三步必须在一个事务里,否则会出现订单生成但排期没更新的脏数据。判断方式是翻applicationContext.xml里<tx:method name="insert*" propagation="REQUIRED"/>这类配置,看到以insert、update、delete开头的方法都强制走事务就说明这块处理得没问题。
3. EasyUI 表格 + SpringMVC:服务预约模块的前后端打通
3.1 EasyUI datagrid 分页参数和控制层的对应关系
EasyUI 的 datagrid 组件请求后台时,默认会带上page和rows两个参数,但很多 SSM 项目里 mapper 用的字段名是start和rows,于是需要在 Controller 层做一次参数转换。这个转换逻辑是整个项目里最容易讲明白、也最容易翻车的地方。
Controller 层常见做法是接收page和rows,手动算出 start:
@RequestMapping("/admin/order/list") @ResponseBody public Map<String, Object> list( @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "rows", defaultValue = "10") Integer rows, @RequestParam(value = "status", required = false) String status, @RequestParam(value = "elderName", required = false) String elderName) { int start = (page - 1) * rows; Map<String, Object> params = new HashMap<>(); params.put("start", start); params.put("rows", rows); params.put("status", status); params.put("elderName", elderName); List<ServiceOrder> orderList = orderService.selectOrderList(params); int total = orderService.countOrderList(params); Map<String, Object> result = new HashMap<>(); result.put("total", total); result.put("rows", orderList); return result; }这段代码的意义在于,后台返回的 JSON 结构被硬性要求为{total: 总记录数, rows: 当前页数据},EasyUI 的 datagrid 只认这个格式。字段名不能写成data或list,一旦写错,表格永远显示空白,控制台没有报错,这种情况我见过好几个同学卡了一下午。
分页参数还有一个边界问题:page从 1 开始,而 SQL 里LIMIT start, rows的 start 从 0 开始,所以换算公式是(page - 1) * rows。老手一般不会忘,但答辩被问到「为什么 page=1 时 start 不是 1」时,能答出这一步通常就加分了。
3.2 服务下单与工单状态流转的接口写法
服务预约模块是整个系统里的业务重心,至少涉及三个接口:查询可预约服务、提交预约、查询我的工单。其中提交预约的接口最有拆解价值,因为它同时要写两张表。
下面这段是典型的 Service 层处理逻辑:
@Service public class ServiceOrderServiceImpl implements ServiceOrderService { @Autowired private ServiceOrderMapper orderMapper; @Autowired private ServiceItemMapper itemMapper; @Transactional(rollbackFor = Exception.class) @Override public boolean createOrder(ServiceOrder order) { // 1. 校验服务项目是否存在且处于上架状态 ServiceItem item = itemMapper.selectByPrimaryKey(order.getServiceId()); if (item == null) { throw new BusinessException("服务项目不存在"); } if (item.getStatus() != 1) { throw new BusinessException("该服务已暂停预约"); } // 2. 插入工单记录,初始状态为待分配 order.setStatus(0); order.setOrderNo(generateOrderNo()); orderMapper.insertSelective(order); // 3. 更新服务项目预测量 itemMapper.increaseBookCount(order.getServiceId()); return true; } }这段代码有三个细节值得答辩时拿出来讲:一是用@Transactional保证两步操作同生共死;二是通过BusinessException抛出业务错误,由全局异常处理器统一转成 JSON 返回给前端,而不是让异常直接打到 Tomcat 上;三是工单号由generateOrderNo()生成,常见的格式是时间戳加随机数。
状态流转的部分通常放在工单列表和详情页里。status字段我见过两种设计,第一种是 0-待分配、1-已接单、2-服务中、3-已完成,第二种是直接存字符串。数字枚举更好做下拉筛选,字符串在展示时更友好。这个项目用数字类型,在 Mapper 里通过CASE WHEN转成中文再返回给前端,SQL 里这样写:
SELECT order_id, CASE status WHEN 0 THEN '待分配' WHEN 1 THEN '已接单' WHEN 2 THEN '服务中' WHEN 3 THEN '已完成' ELSE '已取消' END AS statusText FROM service_order3.3 Bootstrap 页面混用时的两个细节
混搭架构本身没问题,但两个细节要留意。第一是 jQuery 冲突。EasyUI 和 Bootstrap 都依赖 jQuery,如果在一个页面里同时引入两套框架的 js 文件,弹窗组件可能会互相覆盖。这个项目里 portal 页面只引 Bootstrap,admin 页面只引 EasyUI,互不干扰,这是对的。
第二个细节是表单提交方式。Bootstrap 的门户页面里,预约表单用的是普通form同步提交,后台Controller接收后return "redirect:/order/list"。EasyUI 后台页面的操作则全部走$.ajax或者$.post,弹messager.alert提示。两种机制并存,新手容易混,改代码时要先分清自己在改哪一侧。
我拆这个项目时发现一个体验细节:portal 页面里有个「我要预约」的按钮,点击后如果没登录,会先跳转登录页,登录成功再跳回预约页。SSM 项目里这个逻辑通常用springmvc.xml里的拦截器实现,拦截所有/order/**请求,判断 session 中是否存在登录用户。
4. 部署运行与避坑指南:从导入到跑通完整路径
4.1 Eclipse + Tomcat + MySQL 的导入顺序
拿到压缩包后的第一件事不是解压,而是确认环境版本。这个项目基于 JDK 1.8、Tomcat 8.5、MySQL 5.7 编译运行,如果你本机装的是 JDK 17 或 Tomcat 10,直接跑大概率报错。Tomcat 10 之后把javax.servlet换成了jakarta.servlet,SSM 老项目基本都跑不起来的。
导入步骤我建议按这个顺序来:
- 解压源码包,确认目录里有
pom.xml(Maven 项目)还是.classpath加lib目录(普通 Dynamic Web Project)。看到.classpath说明原始项目是在 Eclipse 里建的工程。 - Eclipse 里
File -> Import -> Existing Projects into Workspace,选择解压后的根目录。 - 打开
jdbc.properties,把数据库地址、账号、密码改成自己本机的,重点看 URL 里的characterEncoding=utf8是否配置。 - 用 Navicat 或命令行执行 SQL 脚本,确认数据库名和代码里
jdbc.url写的库名一致。 - 右键项目
Run As -> Run on Server,选择本机配置好的 Tomcat 8.5。
一个容易踩的点:Maven 项目没有lib目录,依赖全部在pom.xml里,导入后如果不触发Maven -> Update Project,项目会一直报红叉。普通 Web 项目则相反,lib目录下必须能看到所有 jar 包,少一个都起不来。
4.2 数据库脚本导入:编码、库名与初始账号
数据库脚本是整个项目能不能起来的关键,95% 的部署失败都发生在这一步。执行脚本时注意三件事。
编码问题。脚本文件如果是 UTF-8 编码,而 Navicat 默认按 GBK 读取,导入后中文全部乱码。我一般用命令行导入,执行前先指定编码:
mysql -u root -p --default-character-set=utf8 < db_community_elderly.sql库名不一致的问题。脚本里如果写了CREATE DATABASE,导入后要检查实际库名和jdbc.properties里的库名是不是同一个。如果脚本只建表不建库,那就得手动先建库再导入:
CREATE DATABASE IF NOT EXISTS community_elderly DEFAULT CHARSET utf8mb4; USE community_elderly; SOURCE /完整路径/db_community_elderly.sql;初始账号的问题。系统管理员的账号密码写在脚本的INSERT语句里,但密码是 MD5 加密后的密文,不是明文。你需要先去数据库查这条记录确认账号名,比如admin / 123456,把123456做一次 MD5 后和库里的值比对,对上了说明初始密码就是它。
4.3 五个真实踩坑记录
以下五条来自我拆 SSM 毕设项目时的实际经验,每一条都有人反复踩。
第一个是 Tomcat 启动后项目打不开,404 白屏。现象:控制台无报错,浏览器访问http://localhost:8080/项目名一直 404。原因:项目部署名和实际访问路径不一致。解决:右键项目Properties -> Web Project Settings,把Context Root改成短一点的名字,或者直接在 Tomcat 的 server.xml 里加<Context path="/elderly" docBase="项目名" />。
第二个是访问后台页面没有样式。现象:页面 HTML 出来了但全是纯文本,css、js 文件 404。原因:springmvc.xml里配置了拦截所有请求,静态资源被拦住了。解决:在配置里放行静态资源,常见写法:
<mvc:resources mapping="/static/**" location="/static/"/> <mvc:resources mapping="/css/**" location="/css/"/> <mvc:resources mapping="/js/**" location="/js/"/>这个坑几乎每个 SSM 项目都会遇到,答辩时一定要能说出来。
第三个是列表页加载不出来,F12 看到后台返回 500。现象:接口报BadSqlGrammarException。原因大多是表名或字段名不匹配,比如service_item表里实际字段叫service_price,Mapper 写的却是service_money。解决:打开Navicat比对表结构,把 Mapper XML 里的字段改过来。动手前先desc 表名看一眼。
第四个是中文乱码,前台页面所有中文显示为问号。现象:jsp 页面顶部已经写了pageEncoding="UTF-8"但还乱码。原因:Tomcat 的 connector 默认编码不是 UTF-8。解决:在server.xml里找到 Connector 节点,加上URIEncoding="UTF-8"。
第五个是后台能登录但门户页跳不过去。现象:登录成功后跳转地址出现后台相对路径,门户页 404。原因基本是 Controller 里跳转用的是绝对路径/portal/index,而部署名含项目名时需要拼接。解决:改用redirect:/portal/index并且检查base标签是否已设置上下文路径。
5. 毕业设计答辩的加分技巧:十分钟演示路径与一个通用改进点
5.1 演示编排:从登录到工单闭环
答辩演示不要打开项目就随机乱点,十分钟内要讲成一个闭环故事。我拆过不少高分项目,演示路径通常固定为:管理员登录 -> 维护服务项目 -> 模拟老人信息录入 -> 门户页面下单 -> 后台查看工单 -> 更新状态。这条路径走完,评委就看到了完整的角色划分和业务流程。
每个页面停留时准备一个问题。在服务项目列表页,评委大概率会问「分页是怎么实现的」,这时引到 EasyUI 的 page/rows 和 MyBatis LIMIT 的转换上,背熟 3.1 里的那段代码即可。在工单状态更新时,评委问「同时改了两张表怎么办」,答事务管理。这两个问题答住了,项目评分基本不会低。
5.2 一个通用改进点:把服务日历视图加上
如果想让项目有新意,我建议加一个日历视图,突出「养老服务的时间维度」。实现方式是在门户页引入一个轻量日历组件,点击日期后显示当天可预约的护工和时段,预约时同时选择日期和时段,后台工单表增加appoint_date和time_slot两个字段。
这个改动代码量不大,但效果非常明显,答辩开场白可以说「原始项目是普通表单预约,我增加了日历排期视图,让老人家属能直观看到哪天有空余服务」。评委听到这种自己加的模块,通常会往下追问几个实现细节,准备好下面的回答:
- 日历数据从哪来:项目表里每个护工维护一周的排班表。
- 冲突怎么处理:预约时把护工 id 和日期时段做唯一索引,重复提交就报错。
- 数据量大了怎么办:只加载当前月数据,前后两个月靠切换月份时异步加载。
回答完这几个问题,项目就绝不是「跑起来就行」的档次了。
顺手留一个习惯:任何演示开始前,我都要重新执行一遍数据库脚本,清空测试数据,然后用初始账号走一遍完整流程。曾经在答辩前半小时发现库里多了好几条乱写的测试单,临时连 Navicat 清数据,手忙脚乱,从那以后每次演示前都强制走一遍「删库重新导入脚本 -> 重置初始账号 -> 验证首页可访问」三步。这套流程同样适合你,希望帮到你。
本文还有配套的精品资源,点击获取