简介:基于Java SSM、Vue和Android的养老院管理系统源码包,适合毕业设计、课程实训、全栈开发练习或养老机构信息化改造参考。项目以Spring容器管理事务,SpringMVC处理请求分发,MyBatis简化数据持久化,Vue构建响应式后台界面,并接入Android端实现移动查看与管理,核心业务覆盖老人档案、床位分配、护理记录、家属沟通等环节。压缩包共1541个文件,约23.75MB,以Java源码、Vue组件、JavaScript逻辑文件为主体,配以PNG/JPG/SVG图标图片、CSS样式、XML配置、SQL脚本和bat启动命令,目录结构按功能拆分,导入后即可运行调试。目前已有83人学习下载。包内还包含完整的前后端实现、数据库表设计及一键启动脚本,采用前后端分离并复用后端接口供Android调用,既可直接节省环境搭建时间,也可作为理解SSM集成、Vue组件化与移动端数据交互的实战范本。
1. 养老院管理系统源码拆解:Android端、SSM后端与Vue管理端各管什么
一个两百张床位的养老院,护工每天要查房、量血压、记录吃药,办公室要随时知道哪个床位空着、哪位老人换了护理等级,家属又总想第一时间了解老人状态。以前靠纸质本子和微信群,数据对不上,事后也查不到。后来用了这套“Java-ssm-vue基于Android的养老院管理系统”源码,Android端给护工录入,SSM后端统一管接口和权限,Vue管理后台给办公室做床位统计和人员配置,一条链路才真正闭环。
这条技术栈最直接的价值是“三端分离”:业务动作发生在手机端,管理动作发生在后台,数据规则统一收敛在后端。适合有Spring、MyBatis基础、想在Android端落地的开发者,也适合小团队接养老信息化项目时快速起一个最小可用版本。你不需要把每个模块都做到极致,能先把登录、老人档案、护理等级这条主线跑通,就已经能支撑真实业务。
下面按我平时的落地顺序讲:先定表结构,再写后端接口,再接Android端,最后处理联调里的坑。每一步都有最小代码,照着改就能用。
2. 先定数据模型:养老院核心表结构与权限边界
动手写代码前先画表,因为三端都靠同一套数据。很多源码包让人先跑起来再改表,其实容易翻车。养老院系统的核心就三件事:老人档案、床位占用情况、护理等级变化。把这三种关系理清,权限边界才有落脚点。
2.1 老人档案、床位与护理等级:三张主表的关系设计
老人主表里会存姓名、性别、生日、当前床位、当前护理等级、紧急联系人、入院时间、状态。床位单独建表,因为房间号和床号会被多个业务引用,比如调房、统计空床。护理等级不能只在老人表里存一个当前值,还要有变更记录。原因是“现在是什么等级”和“什么时候从半自理变成了全护理”是两个完全不同的需求,只更新一个字段,历史就丢了。所以老人表的bed_id指向床位表的id,变更日志表通过elder_id关联老人。
表关系不复杂,但有一个容易被忽略的点:老人和床位不是强绑定。老人临时离院、转院,床不能一直标记占用,也不能直接删老人记录。我一般会在老人表加status字段,用1-在院,0-离院做逻辑删除,宁可留脏数据,也不能把审计轨迹丢掉。护理等级变更更是如此,每次调级都要写明原因和操作人。
2.2 角色权限:管理员、护工、家属三种账号的菜单与数据隔离
养老院系统不需要太复杂的权限模型,经典的 RBAC 三张表就够用:用户表、角色表、菜单表,再加上用户角色关联和角色菜单关联。但真正要花心思的不是菜单,而是数据隔离。管理员能看到全院数据,护工只能看到自己负责的区域,家属只能看到和自己绑定的老人。如果只在 Vue 端把菜单按钮隐藏,后端接口不限制,护工用 Android 端拼接参数就能查到别人负责的老人,这是安全漏洞。
所以设计表的时候就要预留隔离字段。老人表放一个area_id,护工表放关联的area_id或通过中间表维护。所有列表接口在后端拼接 SQL 时强制带area_id条件,不让前端随便传。管理员要小范围越权时,可以做一个“模拟身份”功能,后端临时把自己切到对应区域,而不是把全量数据暴露出来。
2.3 用 SQL 建表:核心字段与状态枚举
下面是常见的建表语句,覆盖老人、床位和护理等级变更日志三张表:
-- 老人主表:一个老人对应一张在住床位 CREATE TABLE `elder` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '老人姓名', `gender` tinyint DEFAULT '0' COMMENT '0-男 1-女', `birthday` date DEFAULT NULL COMMENT '出生日期', `bed_id` int DEFAULT NULL COMMENT '当前床位ID,关联bed.id', `care_level` tinyint DEFAULT '1' COMMENT '护理等级:1-自理 2-半自理 3-全护理', `guardian_name` varchar(50) DEFAULT NULL COMMENT '紧急联系人', `guardian_phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `area_id` int DEFAULT NULL COMMENT '所属区域/楼层,用于护工数据隔离', `status` tinyint DEFAULT '1' COMMENT '1-在院 0-离院', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人档案表';这段 SQL 里有几个字段是特意设计的:gender用tinyint而不是varchar,省空间且方便前端映射;care_level用固定枚举,不要存“半自理”这种中文,否则改一次枚举值要动一堆数据;bed_id允许为空,因为老人可能临时离院但档案还在;area_id是数据权限的关键过滤字段,后面所有查询都要带上。时间字段用datetime而不是timestamp,因为养老院项目会有历史归档需求,datetime的范围更大,显示也更直观。
-- 床位表 CREATE TABLE `bed` ( `id` int NOT NULL AUTO_INCREMENT, `room_no` varchar(20) NOT NULL COMMENT '房间号', `bed_no` varchar(20) NOT NULL COMMENT '床号', `area_id` int DEFAULT NULL COMMENT '区域/楼层', `status` tinyint DEFAULT '0' COMMENT '0-空闲 1-占用 2-维修', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='床位表'; -- 护理等级变更记录表 CREATE TABLE `care_level_log` ( `id` int NOT NULL AUTO_INCREMENT, `elder_id` int NOT NULL COMMENT '老人ID', `old_level` tinyint NOT NULL, `new_level` tinyint NOT NULL, `reason` varchar(255) DEFAULT NULL COMMENT '变更原因', `operator_id` int NOT NULL COMMENT '操作人', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='护理等级变更日志';care_level_log里的reason我一般要求非空,不为难使用的人,而是逼着操作者想清楚为什么调级。operator_id记录操作人,后面出纠纷时能追责。注意:如果你拿到的源码包自带 SQL,先以包内为准,但字段语义可以参考这套设计,重点看有没有area_id和变更日志,缺的话建议补上再动代码。
3. 后端SSM落地:Controller-Service-Mapper三层与鉴权拦截器
后端是 Android 端和 Vue 管理后台共同依赖的服务。SSM 指 Spring、SpringMVC、MyBatis,虽然现在 Spring Boot 更普及,但很多养老院项目源码仍是 SSM 风格。核心分层是 Controller-Service-Mapper,理解清楚这一层,后面接什么都顺。
3.1 SSM 各层怎么分工:为什么养老院项目不需要微服务
Controller 只做参数接收、路由映射、统一返回,不写业务。Service 放业务规则,比如调床时要同时更新床位状态和写日志,这个动作必须在一个事务方法里,不能散落在 Controller 里。Mapper 只负责 SQL 映射,不做计算。
养老院这个场景并发很低,几十个护工同时在线,一个 SSM 应用加一台 MySQL 足够。选 SSM 而不是微服务,不是因为微服务不好,而是没必要。微服务要拆服务、做注册中心、处理分布式事务,对一个几百人的内部系统来说只会增加运维负担。你甚至可以把整个应用打成一个 war 包扔进 Tomcat,管理后台和 APP 都访问同一个服务,成本最低,排查问题也直观。
3.2 登录与Token鉴权:拦截器里的用户上下文
登录方案我一般用 token,不用 session。原因是 Android 端和 Vue 端都要访问接口,session 依赖 Cookie,在原生 App 里处理麻烦,token 放请求头更通用。登录成功后后端返回一个 UUID 字符串,同时把 token 和用户 ID 存到缓存里,之后请求通过拦截器校验。
拦截器是 SSM 里很常见的写法,继承HandlerInterceptorAdapter,在请求进入 Controller 之前做校验:
public class TokenInterceptor extends HandlerInterceptorAdapter { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登录接口本身不需要校验,先放行,避免死循环 if (request.getRequestURI().contains("/api/login")) { return true; } String token = request.getHeader("token"); if (token == null || !tokenService.checkToken(token)) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录过期\"}"); return false; } UserContext.set(tokenService.getUserIdByToken(token)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后清理当前线程的用户ID,防止线程池复用串数据 UserContext.clear(); } }这段代码里最容易被忽略的是afterCompletion里的清理动作。Tomcat 会复用线程处理多个请求,如果你把用户 ID 放在 ThreadLocal 里不清除,下一个请求会拿到上一个用户的 ID,轻则数据串,重则越权。preHandle里先判断 URI 包含/api/login就放行,否则所有/api/**请求都要先过 token 校验。
拦截器写好后要注册进 SpringMVC:
public class WebConfig implements WebMvcConfigurer { @Autowired private TokenInterceptor tokenInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login"); } }拦截路径用/api/**,登录接口单独排除。这样同一个 token 机制同时覆盖 Android 端和 Vue 管理端,前端不用区分两套登录。如果源码是 XML 配置,就在spring-mvc.xml里用<mvc:interceptors>做同样的事,思路一样。
3.3 一个完整的“老人列表”接口:从Mapper到Controller的代码路径
我习惯先写接口再写页面,这里以“老人列表”为例,把 MyBatis 的动态 SQL 和三层调用串起来。先定义一个 Mapper 接口:
public interface ElderMapper { List<Elder> selectByCondition(@Param("keyword") String keyword, @Param("careLevel") Integer careLevel, @Param("areaId") Integer areaId); }XML 里写动态 SQL,注意status = 1作为默认条件,后续所有AND都接在它后面,不会拼出语法错误:
<select id="selectByCondition" resultType="Elder"> SELECT id, name, gender, birthday, bed_id, care_level, guardian_name, guardian_phone, area_id, status FROM elder WHERE status = 1 <if test="areaId != null"> AND area_id = #{areaId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR guardian_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="careLevel != null"> AND care_level = #{careLevel} </if> ORDER BY id DESC </select>这里有两个细节。CONCAT('%', #{keyword}, '%')而不是'%#{keyword}%',后者在某些 MyBatis 版本下解析会出问题,前者是稳定的写法。#{}是预编译占位符,能防 SQL 注入;不要用${}直接拼接用户输入,尤其 keyword 这种字符串,容易被注入。
Service 层做得很薄,因为当前场景没有复杂事务:
@Service public class ElderServiceImpl implements ElderService { @Autowired private ElderMapper elderMapper; @Override public List<Elder> listElder(String keyword, Integer careLevel, Integer areaId) { return elderMapper.selectByCondition(keyword, careLevel, areaId); } }Controller 暴露接口:
@RestController @RequestMapping("/api/elder") public class ElderController { @Autowired private ElderService elderService; @GetMapping("/list") public Result list(@RequestParam(required = false) String keyword, @RequestParam(required = false) Integer careLevel, @RequestParam(required = false) Integer areaId) { return Result.ok(elderService.listElder(keyword, careLevel, areaId)); } }Result是统一返回体,一般含code、msg、data三个字段,Android 端只要判断code就能决定是否走业务逻辑。areaId现在允许前端传,是方便调试;真正上线时应当从UserContext里取,防止护工传别人的区域 ID。管理员可以传空,护工不能。
4. Android端实现:网络请求、登录态保持与页面骨架
Android 端是这个系统的前场,护工每天打开它干活。它的核心功能就三块:登录、看老人列表、点进详情做查房记录。代码量不大,但网络层和登录态如果不做对,后面整个 App 都会很难调。
4.1 Android端为什么选原生Java + OkHttp/Retrofit而不是HTML5壳
有些项目图省事,直接用 WebView 套 Vue 页面,Android 端只留一个壳。但养老院场景会频繁用到相机拍老人照片、录音记录异常情况,WebView 调这些原生能力要通过桥接,很别扭。护工可能在信号不好的走廊、地下层操作,原生 App 更容易做本地缓存和失败重试,WebView 页面一旦资源加载失败就是白屏。
所以源码用原生 Java 是合理选择。网络层我推荐 OkHttp + Retrofit,Retrofit 用注解声明接口,代码可读性强,底层还是 OkHttp 负责连接。配合 Gson 解析 JSON,开发效率比手写HttpURLConnection高一截。Java 版本不需要上协程,Retrofit 的回调足够支撑这个小项目。
4.2 登录与Token存储:SharedPreferences还是File
登录成功后,后端返回token和用户信息。App 侧要把 token 存起来,下次启动直接判断有没有 token,有就进主页,没有就重新登录。最简单的做法是SharedPreferences,它是应用私有目录下的键值对存储,不需要申请存储权限,也不怕别的应用读到。不要写到一个自定义.txt文件里,那要处理路径、权限、异常,纯属给自己挖坑。
public class TokenManager { private static final String SP_NAME = "nursing_home_sp"; private static final String KEY_TOKEN = "token"; private static final String KEY_USER_ID = "userId"; public static void saveLogin(Context context, String token, int userId) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); sp.edit().putString(KEY_TOKEN, token) .putInt(KEY_USER_ID, userId) .apply(); } public static String getToken(Context context) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); return sp.getString(KEY_TOKEN, ""); } public static int getUserId(Context context) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); return sp.getInt(KEY_USER_ID, 0); } public static void logout(Context context) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); sp.edit().clear().apply(); } }apply()是异步落盘,可以在 UI 线程直接调;commit()是同步写,可能卡主线程,一般不推荐。登录成功调用saveLogin,退出登录调用logout,逻辑非常直接。如果项目对安全性要求高,可以换成加密存储,但养老院内部系统有 token 过期兜底,普通SharedPreferences够用。
4.3 老人列表页的网络层与页面骨架:请求、加载中、空数据三态
先定义 Retrofit 接口:
public interface ApiService { @POST("api/login") Call<BaseResponse<LoginData>> login(@Body LoginRequest request); @GET("api/elder/list") Call<BaseResponse<List<Elder>>> listElder(@Query("keyword") String keyword, @Query("careLevel") Integer careLevel); }BaseResponse是统一包装类:
public class BaseResponse<T> { private int code; private String msg; private T data; public boolean isSuccess() { return code == 200; } }接着用 OkHttp 拦截器统一给每个请求加 token:
OkHttpClient okHttpClient = new OkHttpClient.Builder() .addInterceptor(chain -> { Request original = chain.request(); String token = TokenManager.getToken(context); Request.Builder builder = original.newBuilder(); if (!token.isEmpty()) { builder.header("token", token); } return chain.proceed(builder.build()); }) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build();拦截器里判断 token 非空才加,避免登录接口也带一个空的tokenheader,导致后端误判。超时时间给了 10 秒,养老院现场网络不好,可以调到 15 秒,但不要再长,否则页面会长时间卡在加载状态。
Activity 里调列表接口时,要把页面状态拆成加载中、空数据、错误三态:
private void loadElderList() { showLoading(); String keyword = etSearch.getText().toString().trim(); apiService.listElder(keyword, null) .enqueue(new Callback<BaseResponse<List<Elder>>>() { @Override public void onResponse(Call<BaseResponse<List<Elder>>> call, Response<BaseResponse<List<Elder>>> response) { if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) { List<Elder> list = response.body().getData(); if (list == null || list.isEmpty()) { showEmpty("暂无老人数据"); } else { adapter.submit(list); showContent(); } } else { showError("接口返回异常"); } } @Override public void onFailure(Call<BaseResponse<List<Elder>>> call, Throwable t) { showError("网络连接失败"); } }); }onResponse里要同时判断 HTTP 状态码和业务code,二者都通过才展示数据。onFailure不一定是断网,JSON 解析失败也会走这里,所以错误提示用“网络连接失败”这种模糊话术可以接受。三态可以用一个ViewSwitcher维护,加载、空、错误分别对应三个 view,后续加下拉刷新也方便。
5. 联调阶段排查手册:跨域、日期格式与Android本机访问的避坑清单
三端联调是这个项目真正花时间的地方。遇到问题别当玄学,按现象、原因、解决三步走。下面四条是我见过最高频的,也是新手动辄卡半天的点。
5.1 现象:Android模拟器访问本机后端一直超时,浏览器却正常
原因:Android 模拟器里的localhost是它自己,不是开发机。后端跑在开发机的 8080 端口,模拟器访问http://localhost:8080会打到模拟器自身,自然超时。
解决:Android 官方模拟器访问开发机要用http://10.0.2.2:8080;真机调试时执行adb reverse tcp:8080 tcp:8080,之后手机可以继续用http://localhost:8080;如果后端在另一台服务器,就填局域网 IP,并确认手机和服务器在同一个网络。把 baseUrl 放在一个常量类里,改起来一处生效,不要散落在各页面。
5.2 现象:Vue管理端请求接口报跨域错误,接口调试工具却正常
原因:浏览器有同源策略,Vue 开发服务跑在 9528 端口,后端在 8080,响应头里没有 CORS 信息。接口调试工具不走浏览器,所以能调通。这不是后端接口坏了,是浏览器拦截了响应。
解决:在后端 SpringMVC 配置里加一个 CORS 过滤器:
@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("http://localhost:9528"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return new CorsFilter(source); }注意:生产环境不要把来源放开成*,最好限定为管理后台的域名。如果开了setAllowCredentials(true),来源就不能再是*,要用addAllowedOriginPattern精确配置,否则部分浏览器会直接拒绝。如果你不打算动后端,也可以在 Vue 的vue.config.js里配devServer.proxy转发/api,开发环境能规避跨域,但生产环境还是要靠 Nginx 反向代理。
5.3 现象:birthday 字段返回一串数字或带T的UTC时间
原因:后端实体字段如果是java.util.Date,Jackson 默认会序列化成毫秒时间戳,Android 端拿到"1725350400000"这种东西,解析时一个不小心就是乱码;如果实体用了LocalDate,不同 Jackson 版本输出的格式还可能不一样,要么带T,要么带+00:00。
解决:统一日期格式。在 SpringMVC 配置里定制 ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));如果你用的是LocalDate,就在实体字段上加注解:
@JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birthday;Android 端对应字段用String接收,不要试图转成 Date 再转一次时区,多转一次很容易多出 8 小时。
5.4 现象:登录成功之后,Android调其它接口还是401
原因:最常见是请求头名字不一致。后端拦截器读的是token,Android 代码里却写成了Authorization;另一种情况是 token 存在了内存 static 变量里,App 一杀进程就丢;还有可能是 token 有效期太短,刚登录完测试时正好过期。
解决:先统一 request header 名称,都叫token。Android 在 OkHttp 拦截器里用builder.header("token", token);后端拦截器读request.getHeader("token")。排查顺序建议先拿接口调试工具带 token 手动调一次/api/elder/list,通了就说明后端没问题,问题在 Android 端的 header 或 baseUrl。token 有效期养老院场景给 7 天比较合理,后台管理端给 1 天,分开配置。
6. 从源码到自己部署:验证接口连通性的三种手段与后续改造技巧
先把后端启动起来,用 curl 验证接口,是最快的定位手段。登录接口这样测:
curl -X POST http://localhost:8080/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'拿到 token 后,再带 token 打列表接口:
curl "http://localhost:8080/api/elder/list?keyword=&careLevel=1" \ -H "token: 上面返回的token"如果这步都不通,别急着打开 Android Studio,先解决后端问题。这是第一步。
第二步是 Vue 管理端。启动后看浏览器 Network 面板,确认请求是否真的发出。如果看到红色的 CORS Error,说明是跨域;如果请求发出去但状态码是 401,说明 token 没带或不对。Vue 开发阶段建议直接用vue.config.js里的devServer.proxy把/api转发到后端,能少处理很多跨域问题。
第三步是 Android 端。在 OkHttp 上挂一个日志拦截器,把请求和响应打出来看。添加 OkHttp 日志库后用HttpLoggingInterceptor,等级设BODY,能看到每一次实际请求的 URL、header 和响应 JSON。发布版本时一定要把等级设为NONE,否则老人姓名、联系方式这些隐私会全量落进日志。
代码跑通之后,如果要拿这套源码去做真实项目,优先改三个点:第一,所有核心业务表加tenant_id,为以后集团多院区预留隔离;第二,加一张care_task表,把护工的体温、血压、喂药记录存下来,这是实际付费功能;第三,老人呼叫、异常指标推送用 WebSocket 或消息推送,不要一直轮询。
很多同学改这种源码时,第一件事就是去改老人表加字段,结果后面要护理等级变更历史时补日志补到后悔。我自己现在改这类系统前,会先问一句:这个字段变化后需要审计吗?床位调整需要,护理等级调整需要,用药记录需要,唯独“备注”这种不用。想清楚再动手,能省掉后面一大半返工。希望帮到你。
本文还有配套的精品资源,点击获取