这个基于SpringBoot+Vue的健康检查系统是我在完成计算机专业毕业设计时开发的一个全栈项目。作为一个面向个人健康管理的数字化平台,它整合了健康数据记录、医疗服务预约、用药提醒等核心功能,旨在解决传统健康管理方式中存在的效率低下、数据分散等问题。
在疫情常态化背景下,人们对自身健康的关注度显著提升。根据我的调研,超过70%的受访者表示需要一款能够集中管理各类健康信息的数字化工具。而现有的健康类APP往往功能单一,要么只关注运动数据,要么仅提供问诊服务,缺乏综合性解决方案。这正是我决定开发这个系统的初衷。
经过仔细评估,我选择了以下技术组合:
前端技术栈:
后端技术栈:
系统采用前后端分离架构,这种设计带来了几个显著优势:
系统整体架构分为四层:
这个模块是系统的核心,实现了健康数据的采集、存储和分析功能。在设计时,我特别考虑了以下方面:
数据结构设计:
java复制@Entity
@Table(name = "health_data")
public class HealthData {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
private LocalDate recordDate;
private Double weight; // 体重(kg)
private Double height; // 身高(cm)
private Integer systolicPressure; // 收缩压(mmHg)
private Integer diastolicPressure; // 舒张压(mmHg)
private Double bloodSugar; // 血糖(mmol/L)
@Column(length = 1000)
private String physicalCondition; // 身体状况描述
// 省略getter/setter
}
关键技术实现:
java复制@PostMapping("/health-data")
public ResponseEntity<?> addHealthData(
@Valid @RequestBody HealthDataDTO healthDataDTO,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest()
.body(bindingResult.getAllErrors());
}
// 处理逻辑...
}
java复制@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void generateDailyHealthReport() {
List<User> users = userRepository.findAll();
users.forEach(user -> {
HealthReport report = healthAnalysisService
.analyzeUserHealth(user.getId());
reportRepository.save(report);
});
}
这个功能模块实现了用户在线预约挂号的全流程,主要包含以下子功能:
java复制@GetMapping("/doctors")
public Page<DoctorDTO> getDoctors(
@RequestParam(required = false) String department,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Specification<Doctor> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (department != null) {
predicates.add(cb.equal(root.get("department"), department));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
return doctorRepository.findAll(spec, PageRequest.of(page, size))
.map(doctorMapper::toDTO);
}
java复制@Transactional
public AppointmentResult makeAppointment(Long userId, Long doctorId,
LocalDateTime appointmentTime) {
// 使用乐观锁防止超订
Doctor doctor = doctorRepository.findByIdWithLock(doctorId)
.orElseThrow(() -> new ResourceNotFoundException("Doctor not found"));
if (!doctor.isAvailableAt(appointmentTime)) {
return AppointmentResult.failed("该时间段已约满");
}
doctor.addAppointment(appointmentTime);
doctorRepository.save(doctor);
Appointment appointment = new Appointment();
appointment.setUser(userRepository.getById(userId));
appointment.setDoctor(doctor);
appointment.setAppointmentTime(appointmentTime);
appointment.setStatus(AppointmentStatus.BOOKED);
appointmentRepository.save(appointment);
return AppointmentResult.success(appointment.getId());
}
用药提醒是确保用户按时服药的关键功能,其实现要点包括:
java复制public class MedicationReminder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private User user;
private String medicationName;
private Dosage dosage; // 剂量信息
@ElementCollection
@CollectionTable(name = "reminder_times")
private Set<LocalTime> reminderTimes; // 每日提醒时间点
private boolean active;
// 提醒方式:APP推送、短信、邮件等
@Enumerated(EnumType.STRING)
private ReminderMethod method;
}
java复制@Service
@RequiredArgsConstructor
public class ReminderService {
private final MedicationReminderRepository reminderRepository;
private final NotificationService notificationService;
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkReminders() {
LocalTime now = LocalTime.now().truncatedTo(ChronoUnit.MINUTES);
reminderRepository.findActiveRemindersForTime(now)
.forEach(reminder -> {
notificationService.sendReminder(
reminder.getUser(),
reminder.getMedicationName(),
reminder.getDosage(),
reminder.getMethod()
);
// 记录已发送提醒
reminder.addSentRecord(now.toLocalDate());
reminderRepository.save(reminder);
});
}
}
为了直观展示用户的健康趋势,我采用了ECharts实现数据可视化。主要挑战在于处理大量历史数据时的性能优化:
解决方案:
java复制public List<HealthDataPoint> getHealthTrend(Long userId,
HealthDataType type, LocalDate start, LocalDate end) {
long days = ChronoUnit.DAYS.between(start, end);
if (days > 365) { // 超过1年按月份采样
return healthDataRepository
.findMonthlyAverages(userId, type, start, end);
} else if (days > 30) { // 超过1个月按周采样
return healthDataRepository
.findWeeklyAverages(userId, type, start, end);
} else {
return healthDataRepository
.findDailyData(userId, type, start, end);
}
}
vue复制<template>
<div ref="chartContainer" class="health-chart">
<div v-if="loading" class="loading">加载中...</div>
<div v-else ref="chart" style="width:100%;height:400px"></div>
</div>
</template>
<script>
import { onMounted, ref, onUnmounted } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';
import * as echarts from 'echarts';
export default {
props: ['userId', 'dataType'],
setup(props) {
const chartContainer = ref(null);
const chart = ref(null);
const loading = ref(true);
let chartInstance = null;
const loadData = async () => {
loading.value = true;
const data = await fetchHealthData(props.userId, props.dataType);
renderChart(data);
loading.value = false;
};
const renderChart = (data) => {
if (!chartInstance && chart.value) {
chartInstance = echarts.init(chart.value);
}
// 配置图表选项...
};
onMounted(() => {
useIntersectionObserver(
chartContainer,
([{ isIntersecting }]) => {
if (isIntersecting && !chartInstance) {
loadData();
}
}
);
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
}
});
return { chartContainer, chart, loading };
}
};
</script>
健康数据属于敏感个人信息,系统安全至关重要。我采取了以下安全措施:
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/health-data/**").hasRole("USER")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
// 密码编码器等配置...
}
java复制@Aspect
@Component
public class DataPrivacyAspect {
@Autowired
private PrivacyService privacyService;
@Around("execution(* com..repository.*.find*(..))")
public Object maskSensitiveData(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
if (result instanceof Optional) {
Optional<?> optional = (Optional<?>) result;
if (optional.isPresent()) {
return Optional.of(privacyService.mask(optional.get()));
}
}
else if (result instanceof Collection) {
return ((Collection<?>) result).stream()
.map(privacyService::mask)
.collect(Collectors.toList());
}
else if (result != null) {
return privacyService.mask(result);
}
return result;
}
}
为确保系统质量,我实施了多层次的测试:
java复制@ExtendWith(MockitoExtension.class)
class HealthAnalysisServiceTest {
@Mock
private HealthDataRepository healthDataRepository;
@InjectMocks
private HealthAnalysisServiceImpl healthAnalysisService;
@Test
void analyzeBloodPressure_shouldReturnWarning_whenHighPressure() {
// 准备测试数据
HealthData data = new HealthData();
data.setSystolicPressure(150);
data.setDiastolicPressure(95);
// 模拟依赖
when(healthDataRepository.findRecentData(anyLong(), any()))
.thenReturn(List.of(data));
// 执行测试
HealthReport report = healthAnalysisService.analyzeUserHealth(1L);
// 验证结果
assertThat(report.getWarnings())
.anyMatch(w -> w.contains("血压偏高"));
}
}
java复制@Testcontainers
@DataJpaTest
class HealthDataRepositoryIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private HealthDataRepository repository;
@Test
void shouldSaveAndRetrieveHealthData() {
HealthData data = new HealthData();
// 设置数据属性...
HealthData saved = repository.save(data);
Optional<HealthData> found = repository.findById(saved.getId());
assertThat(found).isPresent();
assertThat(found.get().getSystolicPressure())
.isEqualTo(data.getSystolicPressure());
}
}
javascript复制describe('Health Data Management', () => {
beforeEach(() => {
cy.login('testuser', 'password');
cy.visit('/health-data');
});
it('should allow adding new health record', () => {
cy.get('[data-test="add-record-btn"]').click();
cy.get('[data-test="weight-input"]').type('70');
cy.get('[data-test="pressure-systolic"]').type('120');
// 填写其他字段...
cy.get('[data-test="submit-btn"]').click();
cy.contains('数据保存成功').should('be.visible');
cy.get('[data-test="records-list"] li')
.should('have.length.gt', 0);
});
});
针对系统性能瓶颈,我实施了以下优化措施:
sql复制CREATE INDEX idx_health_data_user_date ON health_data(user_id, record_date);
java复制@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "healthReports")
public class HealthAnalysisServiceImpl implements HealthAnalysisService {
private final HealthDataRepository healthDataRepository;
@Override
@Cacheable(key = "#userId + '-' + T(java.time.LocalDate).now()")
public HealthReport analyzeUserHealth(Long userId) {
// 耗时的分析逻辑...
}
@Override
@CacheEvict(key = "#userId + '*'")
public void clearUserCache(Long userId) {
// 清除缓存
}
}
javascript复制const HealthChart = () => import('./components/HealthChart.vue');
采用Docker容器化部署方案,Dockerfile配置如下:
dockerfile复制# 构建阶段
FROM maven:3.8.4-openjdk-11 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# 运行阶段
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=build /app/target/health-system-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
使用docker-compose编排服务:
yaml复制version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://db:3306/health_system
- DB_USER=root
- DB_PASSWORD=securepassword
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=securepassword
- MYSQL_DATABASE=health_system
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
nginx:
image: nginx:1.21
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./frontend-dist:/usr/share/nginx/html
volumes:
db_data:
Nginx配置示例:
nginx复制server {
listen 80;
server_name health.example.com;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
在完成这个毕业设计项目的过程中,我积累了一些宝贵的经验:
需求分析的重要性:初期花费两周时间进行详细的需求调研和分析,这为后续开发节省了大量时间。我创建了完整的用户故事地图和功能清单,确保没有遗漏重要需求。
API设计先行:在开始编码前,先用Swagger设计并文档化了所有API接口。这种做法使得前后端开发可以并行进行,大大提高了开发效率。
代码质量保障:
性能考量:在开发中期进行了压力测试,发现健康数据查询接口在并发量高时响应缓慢。通过添加Redis缓存和数据库索引,将响应时间从1200ms降低到了200ms左右。
安全实践:
这个项目让我深刻理解了全栈开发的挑战和乐趣。从数据库设计到前端交互,从业务逻辑到系统安全,每个环节都需要仔细考虑和不断优化。虽然过程中遇到了不少困难,但解决问题的过程正是最好的学习机会。