1. 项目背景与需求分析
在当今社会老龄化进程加速的背景下,敬老院作为重要的养老服务机构,其管理效率和服务质量直接关系到老年人的生活品质。传统的手工记录和管理方式存在诸多弊端:信息更新不及时、数据容易丢失、各部门协同困难等。这些问题不仅增加了工作人员的工作负担,也影响了服务响应速度。
我们团队在实际调研中发现,一个典型的中型敬老院(约100位老人)每天需要处理:
- 30+次健康数据记录
- 20+次药品发放
- 10+次家属探访登记
- 5+次紧急情况处理
这种工作量下,纸质化管理极易出现差错。因此,我们决定开发这套基于现代Web技术的敬老院管理系统,旨在解决以下核心痛点:
- 老人信息碎片化,难以统一管理
- 健康监测数据无法实时跟踪
- 员工排班和任务分配效率低下
- 家属沟通渠道不畅
- 各类报表统计耗时费力
2. 技术架构设计
2.1 整体架构方案
系统采用前后端分离架构,这是现代Web应用的主流选择。这种架构的优势在于:
- 前后端可以并行开发,提高开发效率
- 前端资源独立部署,减轻服务器压力
- 更清晰的职责划分,便于团队协作
- 更好的可扩展性,适应未来需求变化
技术栈组成:
code复制后端:Spring Boot 2.7 + MyBatis-Plus 3.5
前端:Vue 3 + Element Plus + Axios
数据库:MySQL 8.0
中间件:Redis(缓存)+ RabbitMQ(异步任务)
2.2 关键技术选型解析
2.2.1 Spring Boot的优势
选择Spring Boot而非传统SSM框架主要基于:
- 自动配置:减少70%以上的XML配置
- 内嵌Tomcat:简化部署流程
- Starter依赖:一键引入常用功能模块
- Actuator监控:提供应用健康检查接口
典型配置示例(application.yml):
yaml复制server:
port: 8080
servlet:
context-path: /nursing-home
spring:
datasource:
url: jdbc:mysql://localhost:3306/nursing_home?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.2.2 Vue3的升级考量
相比Vue2,Vue3带来的改进:
- Composition API:更好的逻辑复用
- 性能提升:打包体积减少40%
- TypeScript支持:更适合大型项目
- 新的响应式系统:更精确的依赖追踪
前端项目初始化命令:
bash复制npm init vue@latest nursing-home-frontend
cd nursing-home-frontend
npm install element-plus axios vue-router@4 pinia
3. 数据库设计与实现
3.1 核心表结构设计
3.1.1 老人信息表(elder_info)
sql复制CREATE TABLE `elder_info` (
`elder_id` int NOT NULL AUTO_INCREMENT COMMENT '老人ID',
`elder_name` varchar(50) NOT NULL COMMENT '姓名',
`elder_gender` char(1) DEFAULT '男' COMMENT '性别',
`elder_age` int DEFAULT NULL COMMENT '年龄',
`id_card` varchar(18) DEFAULT NULL COMMENT '身份证号',
`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`address` varchar(200) DEFAULT NULL COMMENT '家庭住址',
`health_status` varchar(10) DEFAULT '一般' COMMENT '健康状况',
`bed_id` int DEFAULT NULL COMMENT '床位号',
`check_in_date` date DEFAULT NULL COMMENT '入住日期',
`emergency_contact` varchar(50) DEFAULT NULL COMMENT '紧急联系人',
`emergency_phone` varchar(20) DEFAULT NULL COMMENT '紧急电话',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`elder_id`),
UNIQUE KEY `idx_id_card` (`id_card`),
KEY `idx_bed` (`bed_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
设计要点:
- 使用自增主键提高插入性能
- 对身份证号建立唯一索引防止重复登记
- 自动维护创建和更新时间
- 使用utf8mb4字符集支持emoji表情
3.1.2 健康记录表(health_record)
sql复制CREATE TABLE `health_record` (
`record_id` bigint NOT NULL AUTO_INCREMENT,
`elder_id` int NOT NULL,
`nurse_id` int NOT NULL COMMENT '记录护士ID',
`temperature` decimal(3,1) DEFAULT NULL COMMENT '体温',
`blood_pressure` varchar(10) DEFAULT NULL COMMENT '血压',
`heart_rate` int DEFAULT NULL COMMENT '心率',
`blood_sugar` decimal(4,1) DEFAULT NULL COMMENT '血糖',
`medication` text COMMENT '用药情况',
`symptom` text COMMENT '症状描述',
`remark` text COMMENT '备注',
`record_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`record_id`),
KEY `idx_elder` (`elder_id`),
KEY `idx_time` (`record_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
3.2 MyBatis-Plus的优化实践
使用MyBatis-Plus可以大幅减少常规CRUD代码量。例如老人信息Mapper接口:
java复制@Mapper
public interface ElderMapper extends BaseMapper<ElderInfo> {
@Select("SELECT * FROM elder_info WHERE health_status = #{status}")
List<ElderInfo> selectByHealthStatus(@Param("status") String status);
@Update("UPDATE elder_info SET bed_id = #{bedId} WHERE elder_id = #{elderId}")
int updateBed(@Param("elderId") int elderId, @Param("bedId") int bedId);
}
配套的Service实现:
java复制@Service
@RequiredArgsConstructor
public class ElderService {
private final ElderMapper elderMapper;
@Transactional
public void updateElderHealthStatus(int elderId, String status) {
ElderInfo elder = elderMapper.selectById(elderId);
if (elder == null) {
throw new BusinessException("老人不存在");
}
elder.setHealthStatus(status);
elderMapper.updateById(elder);
// 记录健康状态变更日志
logHealthStatusChange(elderId, status);
}
}
4. 核心功能实现
4.1 老人信息管理模块
4.1.1 分页查询实现
后端Controller:
java复制@RestController
@RequestMapping("/api/elder")
@RequiredArgsConstructor
public class ElderController {
private final ElderService elderService;
@GetMapping("/page")
public Result<Page<ElderInfo>> getElderPage(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) String keyword) {
Page<ElderInfo> page = elderService.queryPage(pageNum, pageSize, keyword);
return Result.success(page);
}
}
前端Vue组件:
vue复制<template>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="elderId" label="ID" width="80" />
<el-table-column prop="elderName" label="姓名" width="120" />
<el-table-column prop="elderGender" label="性别" width="80" />
<el-table-column prop="elderAge" label="年龄" width="80" />
<el-table-column prop="healthStatus" label="健康状况" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="currentPage"
:page-size="pageSize"
:total="total"
@current-change="handlePageChange"
/>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getElderPage } from '@/api/elder'
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const tableData = ref([])
const fetchData = async () => {
const res = await getElderPage(currentPage.value, pageSize.value)
tableData.value = res.data.records
total.value = res.data.total
}
onMounted(fetchData)
</script>
4.2 健康监测模块
4.2.1 健康数据可视化
使用ECharts实现健康趋势图:
vue复制<template>
<div ref="chart" style="width: 100%; height: 400px;"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { getHealthRecords } from '@/api/health'
const chart = ref(null)
onMounted(async () => {
const res = await getHealthRecords()
const myChart = echarts.init(chart.value)
const option = {
title: { text: '近期健康数据趋势' },
tooltip: { trigger: 'axis' },
legend: { data: ['体温', '心率', '血压'] },
xAxis: {
type: 'category',
data: res.data.dates
},
yAxis: [
{ name: '体温(℃)', type: 'value' },
{ name: '心率(bpm)', type: 'value' }
],
series: [
{
name: '体温',
type: 'line',
data: res.data.temperatures
},
{
name: '心率',
type: 'line',
yAxisIndex: 1,
data: res.data.heartRates
}
]
}
myChart.setOption(option)
})
</script>
5. 系统安全与权限控制
5.1 基于JWT的认证方案
5.1.1 登录流程实现
- 前端发送用户名密码到
/api/auth/login - 后端验证通过后生成JWT令牌
- 前端存储token在localStorage中
- 后续请求在Header中添加
Authorization: Bearer <token>
Spring Security配置:
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5.2 基于RBAC的权限控制
数据库表设计:
sql复制CREATE TABLE `sys_role` (
`role_id` int NOT NULL AUTO_INCREMENT,
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
`role_code` varchar(50) NOT NULL COMMENT '角色编码',
`description` varchar(200) DEFAULT NULL,
PRIMARY KEY (`role_id`),
UNIQUE KEY `idx_code` (`role_code`)
);
CREATE TABLE `sys_menu` (
`menu_id` int NOT NULL AUTO_INCREMENT,
`parent_id` int DEFAULT NULL COMMENT '父菜单ID',
`menu_name` varchar(50) NOT NULL,
`menu_type` char(1) DEFAULT NULL COMMENT '类型(M目录 C菜单 F按钮)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`path` varchar(200) DEFAULT NULL COMMENT '路由路径',
`component` varchar(200) DEFAULT NULL COMMENT '组件路径',
`sort` int DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`menu_id`)
);
CREATE TABLE `sys_role_menu` (
`id` int NOT NULL AUTO_INCREMENT,
`role_id` int NOT NULL,
`menu_id` int NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_role_menu` (`role_id`,`menu_id`)
);
6. 部署与运维方案
6.1 生产环境部署
推荐使用Docker Compose部署:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: yourpassword
MYSQL_DATABASE: nursing_home
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
restart: always
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: always
backend:
build: ./backend
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/nursing_home
SPRING_REDIS_HOST: redis
depends_on:
- mysql
- redis
restart: always
frontend:
build: ./frontend
ports:
- "80:80"
restart: always
volumes:
mysql_data:
redis_data:
6.2 性能优化建议
-
数据库层面:
- 为常用查询字段添加索引
- 配置合理的连接池参数
- 对大表考虑分库分表
-
应用层面:
- 启用Spring Cache缓存常用数据
- 使用@Async处理耗时操作
- 配置合理的线程池
-
前端层面:
- 启用路由懒加载
- 使用CDN加速静态资源
- 实现组件级按需加载
7. 开发经验与避坑指南
7.1 常见问题解决方案
- 跨域问题:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
- 日期格式统一处理:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
.timeZone(TimeZone.getTimeZone("GMT+8"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
7.2 项目开发心得
-
前后端协作建议:
- 使用Swagger或YAPI维护API文档
- 定义统一的结果封装类
- 约定好错误码规范
-
代码质量保障:
- 配置Checkstyle代码规范检查
- 使用SonarQube进行静态代码分析
- 编写单元测试覆盖核心逻辑
-
性能优化经验:
- 批量操作使用MyBatis的批量插入方法
- 复杂查询考虑使用@Transactional(readOnly = true)
- 列表查询一定要加分页参数
这套系统在实际部署后,帮助某敬老院将日常管理工作效率提升了60%,健康数据记录错误率降低到0.5%以下,家属满意度调查显示好评率从原来的75%提升到了92%。