尧图精选

基于SSM+Vue+Android的养老院管理系统开发实践

🕒 发布时间:2026/9/16 16:54:14 📁 来源:尧图网络
简介一套基于 Java、SSM、Vue 与 Android 的养老院管理系统源码面向需要学习或开发管理系统的开发者旨在借助信息技术提升养老院管理效率。资源整合 SSM 后端、Vue 管理后台、Android 移动端及 MySQL 数据库可覆盖老人信息、床位安排、护理记录等业务适合作为课程设计或项目实践参考。压缩包共 1541 个文件约 23.75MB主要包含 vue 前端页面、java 后端逻辑、js 脚本、png 界面素材以及 xml/css/sql 等配置与数据库脚本另有 1-install.bat 等安装运行脚本目录结构清晰便于按模块检索与学习。目前已有 82 人学习下载。完整可运行的工程源码及其配套数据库脚本可直接用于理解 SSMVueAndroid 整合开发掌握分层架构、前后端交互与移动端适配细节也可作为二次开发或毕业设计的基础工程。1. 养老院管理系统的技术选型为什么是 SSM Vue Android养老院的日常管理离不开三类数据老人的健康档案、护工的排班记录、床位的使用状态。要同时支撑办公室里的 PC 后台和现场人员手里的 Android 手机最常见的小团队方案就是 Java 后端提供接口Vue 做管理端Android 做移动端。这套组合的存量项目和招聘需求都很大SSM Vue Android 本身就是一个完整可跑通的业务架构适合想从 Java 基础过渡到项目实战的开发者也适合需要快速交付的中小型康养机构项目。需要说明的是SSM 不是最新的框架组合恰恰因为它分层清晰、资料充足、排查问题容易才更适合做这类以 CRUD 和权限管理为核心的内部系统。2. 后端先行SSM 框架的分层实现与接口设计2.1 养老院项目里的 SSM 职责边界与事务管理SSM 是 Spring SpringMVC MyBatis 的缩写。Spring 负责管理 Service 层的 Bean 和事务SpringMVC 负责把 URL 请求映射到 Controller 方法MyBatis 负责把 Java 对象和数据库结果互转。在养老院管理系统里后端接口绝大多数是单表或多表联查查询老人列表、新增健康记录、更新床位状态。这类业务用 SSM 写起来直接但容易踩的坑反而是事务配置漏掉导致一个接口里两步写库只成功了一步数据不一致。MyBatis 的 Mapper 接口在运行时是通过 JDK 动态代理生成代理对象来绑定 SQL 的这也是 Java 面试八股文里常考的知识点。理解这一点对于排查明明写了 SQL 却找不到实现很有帮助。现在很多团队的面试题还在追问 SSM 的自动装配和代理机制说明这套框架仍然是一线开发维护老项目时绕不开的基础。Spring 的事务拦截也是基于 AOP 代理实现的如果 Service 方法被同类内部调用Transactional会失效因为这个调用绕过了代理对象。提示只要是先写主表再写明细表的接口必须在 Service 实现类上标注Transactional单纯在 Controller 方法上加没有实际效果。2.2 养老院核心数据表设计与基础 SQL先看表设计。养老院管理系统至少要有这样几张表下面是精简版建表 SQLCREATE TABLE elder ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 姓名, age INT NOT NULL COMMENT 年龄, bed_no VARCHAR(20) COMMENT 床位号, status TINYINT DEFAULT 1 COMMENT 1在住 0退住, contact_phone VARCHAR(20) COMMENT 紧急联系人电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, nurse_id BIGINT NOT NULL, blood_pressure VARCHAR(20) COMMENT 血压, heart_rate INT COMMENT 心率, note VARCHAR(255) COMMENT 备注, record_time DATETIME NOT NULL, INDEX idx_elder_time (elder_id, record_time) ) COMMENT 老人健康记录;这段 SQL 做了三件事把老人主表和健康记录表分开避免把所有字段堆在一张表里通过elder_id关联两张表方便按老人维度查询历史记录在(elder_id, record_time)上建联合索引让查某位老人某时间段健康趋势不会全表扫描。status字段用来做逻辑删除而不是物理删除退住老人的数据还保留着方便后续做入住率统计和回访。床位的状态不需要单独建表在老人表里维护bed_no字段加上一个床位字典表就够了业务上并不复杂。2.3 一个完整接口的实现从 Controller 到 Mapper下面以新增健康记录接口为例展示 SSM 三层的完整写法。先看 ControllerRestController RequestMapping(/api/health) public class HealthRecordController { Autowired private HealthRecordService healthRecordService; PostMapping(/record) public Result addRecord(RequestBody HealthRecord record) { healthRecordService.addRecord(record); return Result.success(); } }Controller 层只负责接收 JSON、返回统一结果业务拼装和校验放在 Service 层。RequestBody会在进入方法前把客户端传来的 JSON 反序列化成HealthRecord对象所以前端字段名必须和 Java 属性保持一致否则会出现字段全部为 null而接口状态码仍然返回 200 的情况。返回的Result是统一包装类一般包含 code、message、data 三个字段Android 端和 Vue 端都能按同一套结构解析。再看 Service 实现Service public class HealthRecordServiceImpl implements HealthRecordService { Autowired private HealthRecordMapper healthRecordMapper; Transactional Override public void addRecord(HealthRecord record) { if (record.getElderId() null) { throw new BusinessException(老人ID不能为空); } if (record.getHeartRate() ! null (record.getHeartRate() 30 || record.getHeartRate() 220)) { throw new BusinessException(心率数值不合理); } healthRecordMapper.insert(record); } }这段代码的关键是Transactional如果insert抛出异常整个方法回滚不会半截写库。查询类型的接口建议用Transactional(readOnly true)MySQL 在这个标记下会有一些执行优化。业务校验放在 Service 而不是 Controller是因为 Android 和 Vue 两个端都可能调这套接口统一的防线应该在服务端。如果护工在 Android 上输入了明显异常的心率这里直接拒绝写入比事后人工改数据可靠得多。最后是 Mapper 接口和 XML 映射public interface HealthRecordMapper { int insert(HealthRecord record); }insert idinsert parameterTypecom.demo.entity.HealthRecord useGeneratedKeystrue keyPropertyid INSERT INTO health_record ( elder_id, nurse_id, blood_pressure, heart_rate, note, record_time ) VALUES ( #{elderId}, #{nurseId}, #{bloodPressure}, #{heartRate}, #{note}, #{recordTime} ) /insertuseGeneratedKeystrue会让 MySQL 自增主键回填到record对象里后面如果还要往关联表写数据可以直接用这个id。#{elderId}是 MyBatis 的预编译占位符完整参数绑定如下表所示注意不要在 XML 里用${}拼字符串那是 SQL 注入的入口。参数类型说明校验规则elderIdLong老人 ID必填nurseIdLong护工 ID必填bloodPressureString血压值选填heartRateInteger心率30 到 220 之间noteString备注选填长度限制recordTimeDate记录时间必填后端接口写完以后用 Postman 测一次POST /api/health/record能通就说明 SSM 这一层的组件装配没有缺。如果报 404先看 SpringMVC 的包扫描路径是否覆盖了 Controller 所在包这是最常犯的配错。3. 管理端开发Vue 后台如何对接 SSM 接口3.1 Vue 安装及环境配置的最小路径管理端给办公室人员用主要做老人档案管理、排班安排、费用统计。Vue 项目本身不会和 SpringMVC 抢端口开发阶段靠 Vite 代理转发解决跨域。环境配置只需要三步npm create vuelatest elder-admin cd elder-admin npm install npm run develder-admin是项目名需要 Node 18 以上版本。npm create vue会引导选择 TypeScript、Router、Pinia 等选项自带 Vite 目录结构。如果你拿到的是别人分享的源码包优先看package.json里的 scripts 和 dependencies依赖装不上时先删掉node_modules和package-lock.json再重新安装比反复解决依赖冲突省时间。3.2 用 Vite 代理把 /api 转发到 SSM 后端Vue 开发服务器默认跑在 5173 端口SSM 后端在 8080直接请求必然跨域。常见做法是前端只访问/api开头的地址Vite 把前缀转发到后端// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });changeOrigin: true会把请求头的 Host 改成后端地址否则有些 Tomcat 配置会拦截掉来源不明的请求。这样前端代码里写axios.get(/api/elder/list)开发环境就直接访问http://localhost:8080/api/elder/list不需要后端额外配置 CORS。生产部署时再让 Nginx 做同样的转发逻辑是一致的。代理参数作用开发环境常用值target后端服务地址http://localhost:8080changeOrigin改写 Host 头为 targettruerewrite路径重写按实际需要配置需要注意rewrite字段。如果 SSM 后端的RequestMapping带/api前缀那么代理配置里就不需要做路径重写如果后端接口没有/api前缀通常要加一行rewrite: (path) path.replace(/^\/api/, )。先确认后端路由再决定是否加 rewrite否则会出现前端 404 而后端日志里根本没有这个请求。3.3 axios 封装和 vue-router 登录拦截管理端几乎每个页面都要带 token 请求接口Axios 实例必须统一封装import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request拦截器做的事情有三件请求发出前从本地存储取 token 放进请求头收到响应后直接剥掉 axios 外层包装返回业务数据后端返回 401 说明 token 过期或失效清掉并跳回登录页。window.location.href是硬跳转避免了在拦截器里 import router 造成的循环依赖。token 过期是养老院这类内部系统最容易遇到的现象护工手机放了一晚上第二天打开接口全部报 401提醒重新登录即可不要把 token 有效期设置得太短。路由守卫用来做页面级权限控制比在每个组件里判断要干净router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })页面里跳转到老人详情时通常用 query 参数携带 ID// 列表页跳转详情 this.$router.push({ path: /elder/detail, query: { id: row.id } }) // 详情页取出参数 const elderId this.$route.query.idquery参数会出现在地址栏中刷新页面不会丢失适合传简单 ID。传对象或敏感信息则用params但params刷新后会丢失这点要区分场景。Element Plus 这类组件库可以快速拼出表格和表单页面注意表格列里的prop要和后端返回字段完全一致否则渲染出来是空列这是 Vue 管理端联调时最高频的白屏原因。4. 移动端Android 客户端跑通从登录到数据列表4.1 Android 项目的网络层Retrofit OkHttpAndroid 端是护工和巡诊护士在养老院现场使用的工具核心操作是搜索老人、录入健康数据、查看排班。网络层最常用的方案是 Retrofit 配合 OkHttpGradle 依赖大致如下implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0版本号以当前稳定版为准。Retrofit 负责把 Java 接口方法转成 HTTP 请求Gson 转换器处理 JSON 反序列化logging-interceptor 打印请求和响应的完整日志。在 Android Studio 里新建工程后把依赖放进app/build.gradle的 dependencies 块然后定义接口public interface ApiService { POST(api/auth/login) CallLoginResponse login(Body LoginRequest request); GET(api/elder/list) CallListElder listElder(Query(page) int page, Query(size) int size); }接口方法注解路径里没有写http://因为 Retrofit 实例创建时会指定 baseUrl路径都基于它拼接。Body会把 Java 对象序列化成 JSON 放进请求体Query则把参数拼到 URL 问号后面。4.2 明文流量、token 存储与网络地址Android 连接后端有两个明显的坑。第一从 Android 9 开始默认禁止明文 HTTP 流量后端接口是http://就必须显式开启。第二模拟器访问宿主机要用10.0.2.2而不是localhost。网络权限和明文开关都在 manifest 里配置uses-permission android:nameandroid.permission.INTERNET / application android:usesCleartextTraffictrue ... usesCleartextTraffictrue只该在开发阶段开上线前必须移除或换成 networkSecurityConfig否则很容易被安全扫描标记。登录成功后拿到的 token简单项目存 SharedPreferences数据敏感就用 AndroidX Security 的 EncryptedSharedPreferences 加密存储。判断标准是token 被拿走能不能直接调接口改他人数据能的话就要加密。网络地址的选择可以用下面这个表对照排查运行环境baseUrl 写法说明模拟器http://10.0.2.2:8080/模拟器访问宿主机专用地址真机同一局域网http://192.168.x.x:8080/电脑的局域网 IP生产环境https://api.example.com/正式域名4.3 RecyclerView 展示健康数据列表Android 页面数据展示的常规组合是 RecyclerView 加自定义 Adapter。下面是 Adapter 的核心骨架public class HealthRecordAdapter extends RecyclerView.AdapterHealthRecordAdapter.ViewHolder { private final ListHealthRecord records new ArrayList(); NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_health_record, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { HealthRecord record records.get(position); holder.tvPressure.setText(record.getBloodPressure()); holder.tvTime.setText(record.getRecordTime()); } Override public int getItemCount() { return records.size(); } public void submitList(ListHealthRecord list) { records.clear(); if (list ! null) { records.addAll(list); } notifyDataSetChanged(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvPressure, tvTime; ViewHolder(View itemView) { super(itemView); tvPressure itemView.findViewById(R.id.tv_pressure); tvTime itemView.findViewById(R.id.tv_time); } } }submitList是给外部调用的数据入口网络线程返回结果后必须在主线程调用否则抛CalledFromWrongThreadException。Retrofit 的enqueue回调本身已经切回主线程但如果项目里用了协程或 RxJava线程切换要额外注意。接口返回的字段名如果和 Java 属性对不上Gson 会静默赋 null这是列表是空的但我确实拿到了 JSON的最常见原因。把 logging-interceptor 的日志打开对比 JSON 里的 key 和HealthRecord的字段名很快能定位。5. 联调排错与上线前要检查的 3 个地方5.1 Android 真机预览与局域网调试真机调试时不能用模拟器的 10.0.2.2要改成电脑的局域网 IP比如http://192.168.1.23:8080/手机和电脑连同一个 WiFi。后端跑在本机的话Windows 防火墙要允许 8080 端口入站。如果换了网络环境 IP 变了可以把 baseUrl 放到BuildConfig字段或者本地配置类里避免每次改代码。5.2 联调阶段最常见的 4 个现象现象优先排查方向Android 提示 401token 没传或已过期检查拦截器注入请求进不了后端检查 Vite 代理 target 和前缀是否匹配接口 200 但字段全是 nullJSON 字段名和 Java 属性不一致时间显示错乱后端 Date 序列化成时间戳或带 T 的字符串需要格式化大部分联调时间都花在字段名和跨域上把后端日志和 OkHttp 日志同时打开逐层对比比反复改代码快得多。5.3 上线前的 3 个验证操作第一移除 Android 的usesCleartextTraffic切换成正式 HTTPS 地址第二Vue 执行npm run build后用 Nginx 托管 dist 目录并把/api代理到 Java 后端第三确认elder_id、record_time这类高频查询字段建了索引。Nginx 的转发配置可以这样写location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }proxy_pass结尾的斜杠决定了/api/elder/list是原样转发还是去掉前缀后转发。如果 SSM 后端 Controller 里写的是RequestMapping(/api)那代理配置和代码前缀要对齐否则生产环境会出现开发环境从未遇到的 404先检查这一处一般能找到原因。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →