我国老龄化进程正在加速推进,60岁以上人口已突破2.8亿,专业养老护理员缺口高达200万。这个数字背后反映的是无数家庭的现实困境——父母年迈需要专业照护,却苦于找不到合适的护理人员;而具备专业技能的护理员也面临就业信息不对称、工作匹配效率低下的问题。
传统的中介服务模式存在几个明显痛点:
针对这些问题,我们设计开发了这套基于SpringBoot+Vue.js的智慧助老直聘平台。系统核心目标是实现"三个即时":
后端技术栈:
选择Spring Boot主要基于以下考虑:
前端技术栈:
Vue.js的优势在于:
系统采用经典的三层架构:
code复制┌───────────────────────────────────────┐
│ 表现层 │
│ (Vue.js + Element Plus + Axios) │
└───────────────┬───────────────────────┘
│ HTTP/HTTPS
┌───────────────▼───────────────────────┐
│ 业务逻辑层 │
│ (Spring Boot + MyBatis-Plus + Redis) │
└───────────────┬───────────────────────┘
│ JDBC
┌───────────────▼───────────────────────┐
│ 数据访问层 │
│ (MySQL + Elasticsearch) │
└───────────────────────────────────────┘
系统主要实体关系如下:
sql复制-- 用户基础表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '加密密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`phone` varchar(20) NOT NULL COMMENT '手机号',
`user_type` tinyint NOT NULL COMMENT '1-护理员 2-机构 3-管理员',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint DEFAULT '1' COMMENT '状态 0-禁用 1-正常',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 护理员扩展信息表
CREATE TABLE `caregiver_info` (
`user_id` bigint NOT NULL,
`gender` tinyint DEFAULT NULL COMMENT '1-男 2-女',
`birth_date` date DEFAULT NULL,
`education` varchar(20) DEFAULT NULL COMMENT '学历',
`work_years` int DEFAULT NULL COMMENT '工作年限',
`certifications` varchar(255) DEFAULT NULL COMMENT '证书,逗号分隔',
`skills` varchar(255) DEFAULT NULL COMMENT '技能标签',
`self_intro` text COMMENT '自我介绍',
`hourly_rate` decimal(10,2) DEFAULT NULL COMMENT '期望时薪',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 机构信息表
CREATE TABLE `institution_info` (
`user_id` bigint NOT NULL,
`institution_name` varchar(100) NOT NULL,
`license_no` varchar(50) DEFAULT NULL COMMENT '营业执照号',
`address` varchar(255) NOT NULL,
`contact_person` varchar(50) NOT NULL,
`contact_phone` varchar(20) NOT NULL,
`service_area` varchar(255) DEFAULT NULL COMMENT '服务区域',
`institution_type` tinyint DEFAULT NULL COMMENT '机构类型',
`description` text COMMENT '机构描述',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
系统核心的智能匹配功能通过多维度加权算法实现:
java复制public class MatchAlgorithm {
// 权重配置
private static final double LOCATION_WEIGHT = 0.3;
private static final double SKILL_WEIGHT = 0.25;
private static final double SALARY_WEIGHT = 0.2;
private static final double RATING_WEIGHT = 0.15;
private static final double EXPERIENCE_WEIGHT = 0.1;
public static double calculateMatchScore(JobPost job, Caregiver caregiver) {
double score = 0;
// 地理位置匹配(同城加分)
if (job.getLocation().equals(caregiver.getPreferredLocation())) {
score += LOCATION_WEIGHT;
}
// 技能匹配度
Set<String> jobSkills = new HashSet<>(Arrays.asList(job.getRequiredSkills()));
Set<String> caregiverSkills = new HashSet<>(Arrays.asList(caregiver.getSkills()));
double skillMatch = (double) intersection(jobSkills, caregiverSkills).size() / jobSkills.size();
score += skillMatch * SKILL_WEIGHT;
// 薪资期望匹配
if (caregiver.getExpectedSalary() <= job.getMaxSalary()) {
score += SALARY_WEIGHT;
} else {
double salaryDiff = 1 - (caregiver.getExpectedSalary() - job.getMaxSalary()) / job.getMaxSalary();
score += Math.max(0, salaryDiff) * SALARY_WEIGHT;
}
// 评价分数
score += (caregiver.getRating() / 5.0) * RATING_WEIGHT;
// 工作经验
score += Math.min(1, caregiver.getExperienceYears() / 10.0) * EXPERIENCE_WEIGHT;
return score;
}
private static <T> Set<T> intersection(Set<T> set1, Set<T> set2) {
Set<T> result = new HashSet<>(set1);
result.retainAll(set2);
return result;
}
}
基于WebSocket实现的即时通讯功能关键代码:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
// 消息处理逻辑
chatMessage.setTimestamp(LocalDateTime.now());
// 保存到数据库
messageService.save(chatMessage);
return chatMessage;
}
}
前端Vue组件实现:
vue复制<template>
<div class="chat-container">
<div class="messages" ref="messages">
<div v-for="msg in messages" :key="msg.id"
:class="['message', msg.sender === currentUser ? 'sent' : 'received']">
<div class="content">{{ msg.content }}</div>
<div class="time">{{ formatTime(msg.timestamp) }}</div>
</div>
</div>
<div class="input-area">
<input v-model="newMessage" @keyup.enter="sendMessage"
placeholder="输入消息...">
<button @click="sendMessage">发送</button>
</div>
</div>
</template>
<script>
import Stomp from 'webstomp-client';
import SockJS from 'sockjs-client';
export default {
data() {
return {
newMessage: '',
messages: [],
stompClient: null
};
},
computed: {
currentUser() {
return this.$store.state.user.id;
}
},
mounted() {
this.connect();
},
methods: {
connect() {
const socket = new SockJS('/ws');
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, () => {
this.stompClient.subscribe('/topic/public', (message) => {
this.messages.push(JSON.parse(message.body));
this.scrollToBottom();
});
});
},
sendMessage() {
if (this.newMessage.trim()) {
const chatMessage = {
sender: this.currentUser,
content: this.newMessage,
timestamp: new Date()
};
this.stompClient.send("/app/chat.send", JSON.stringify(chatMessage));
this.newMessage = '';
}
},
scrollToBottom() {
this.$nextTick(() => {
this.$refs.messages.scrollTop = this.$refs.messages.scrollHeight;
});
},
formatTime(date) {
return new Date(date).toLocaleTimeString();
}
}
};
</script>
合同签署流程状态机设计:
java复制public enum ContractState {
DRAFT("草稿"),
PENDING_SIGNATURE("待签署"),
PARTIALLY_SIGNED("部分签署"),
FULLY_SIGNED("已完成"),
REJECTED("已拒绝"),
EXPIRED("已过期");
private final String description;
ContractState(String description) {
this.description = description;
}
}
@Service
public class ContractService {
@Transactional
public void initiateContract(Long jobId, Long caregiverId) {
// 验证双方资格
JobPost job = jobRepository.findById(jobId)
.orElseThrow(() -> new BusinessException("岗位不存在"));
User caregiver = userRepository.findById(caregiverId)
.orElseThrow(() -> new BusinessException("用户不存在"));
// 创建合同草稿
Contract contract = new Contract();
contract.setJobId(jobId);
contract.setInstitutionId(job.getInstitutionId());
contract.setCaregiverId(caregiverId);
contract.setStatus(ContractState.DRAFT);
contract.setTemplateId(getStandardTemplateId());
contract.setCreateTime(LocalDateTime.now());
contract.setExpireTime(LocalDateTime.now().plusDays(7));
// 生成合同内容
String content = generateContractContent(job, caregiver);
contract.setContent(content);
contractRepository.save(contract);
// 发送通知
notificationService.sendContractNotification(
caregiverId,
"您有一份新的合同待签署",
"/contract/" + contract.getId());
}
@Transactional
public void signContract(Long contractId, Long userId, String signature) {
Contract contract = contractRepository.findById(contractId)
.orElseThrow(() -> new BusinessException("合同不存在"));
if (contract.getStatus() != ContractState.PENDING_SIGNATURE) {
throw new BusinessException("合同当前状态不可签署");
}
// 验证签署人身份
if (userId.equals(contract.getCaregiverId())) {
contract.setCaregiverSign(signature);
contract.setCaregiverSignTime(LocalDateTime.now());
} else if (userId.equals(contract.getInstitutionId())) {
contract.setInstitutionSign(signature);
contract.setInstitutionSignTime(LocalDateTime.now());
} else {
throw new BusinessException("您无权签署此合同");
}
// 更新合同状态
if (contract.getCaregiverSign() != null && contract.getInstitutionSign() != null) {
contract.setStatus(ContractState.FULLY_SIGNED);
// 触发后续流程
jobService.markPositionFilled(contract.getJobId());
} else {
contract.setStatus(ContractState.PARTIALLY_SIGNED);
}
contractRepository.save(contract);
}
}
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/caregiver/**").hasAnyRole("CAREGIVER", "ADMIN")
.antMatchers("/api/institution/**").hasAnyRole("INSTITUTION", "ADMIN")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
数据安全
日志与审计
java复制@Service
@CacheConfig(cacheNames = "jobCache")
public class JobService {
@Cacheable(key = "#id")
public JobPost getJobById(Long id) {
return jobRepository.findById(id).orElse(null);
}
@CacheEvict(key = "#job.id")
public void updateJob(JobPost job) {
jobRepository.save(job);
}
}
数据库优化
前端性能优化
javascript复制// 搜索框防抖处理
methods: {
searchJobs: _.debounce(function() {
this.loadJobs(this.searchQuery)
}, 500)
}
开发环境
生产环境建议
dockerfile复制# Dockerfile示例
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
bash复制# 构建生产环境代码
npm run build
# Nginx配置示例
server {
listen 80;
server_name yourdomain.com;
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
}
建议配置以下监控指标:
挑战1:高并发下的系统稳定性
挑战2:复杂搜索条件的实现
在实际开发过程中,我们发现系统初期最难的不是技术实现,而是如何建立用户信任。为此我们增加了以下措施:
对于准备开发类似系统的同学,我的建议是: