UserCF 协同过滤算法:3种相似度度量(Jaccard/余弦/皮尔逊)的Python实现与性能对比
2026/7/6 23:10:13
当初做医院挂号系统时,光“医生排班”和“号源库存”的并发处理就卡了5天——多个用户同时抢同一个医生的号时没加锁,导致“一号多挂”,导师一句“医疗系统业务逻辑必须严谨”让我重构了整个预约模块😫。今天把挂号系统的核心业务流程、并发控制、时间冲突检测说透,让你轻松搞定医疗类毕设!
刚开始我想做“智能分诊推荐”、“病历管理”、“在线问诊”,结果导师说“挂号系统的核心是号源管理、预约冲突检测、支付流程,不是扩展功能”。后来调研发现,患者最需要的是快速挂上号、清晰的时间安排、可靠的预约确认。
系统三类用户,权限要严格:
患者端(核心体验):
医生端(管理核心):
管理员端(系统维护):
挂号系统最怕数据不一致。推荐SpringBoot 2.7 + MySQL 8.0(事务隔离级别RR)+ Redis分布式锁 + Vue2。
| 技术 | 为什么选 | 避坑提醒 |
|---|---|---|
| MySQL 8.0 | 支持行级锁,事务隔离级别可配置 | 一定用InnoDB引擎,MyISAM不支持事务 |
| Redis分布式锁 | 解决并发抢号问题 | 别用synchronized,集群部署会失效 |
| Quartz定时任务 | 定时释放过期未支付号源 | 别用Timer,功能太弱 |
| Vue2 + ElementUI | 时间选择组件丰富,适合排班管理 | 日期选择用el-date-picker |
# application.ymlspring:redis:host:localhostport:6379password:lettuce:pool:max-active:8max-wait:-1msmax-idle:8min-idle:0挂号系统最复杂的是时间管理。我当初设计的表结构没考虑“医生临时停诊”,结果患者约了号医生却不在。
必做核心表:
选做扩展表:
CREATETABLEdoctor_schedule(idBIGINTPRIMARYKEYAUTO_INCREMENT,doctor_idBIGINTNOTNULLCOMMENT'医生ID',schedule_dateDATENOTNULLCOMMENT'排班日期',time_slotTINYINTNOTNULLCOMMENT'时间段:1上午,2下午',total_countINTDEFAULT20COMMENT'总号源数',remaining_countINTDEFAULT20COMMENT'剩余号源数',is_cancelledTINYINTDEFAULT0COMMENT'是否停诊:0正常,1停诊',UNIQUEKEYuk_doctor_date_slot(doctor_id,schedule_date,time_slot))COMMENT='医生排班表';重要约束:
publicenumAppointmentStatus{PENDING_PAYMENT(0,"待支付"),// 下单未支付RESERVED(1,"已预约"),// 支付成功CANCELLED(2,"已取消"),// 用户取消COMPLETED(3,"已完成"),// 已就诊EXPIRED(4,"已过期");// 超时未支付// 状态流转:0→1(支付),0→2(取消),0→4(超时),1→2(取消),1→3(就诊)}挂号系统的核心难点是“高并发下的数据一致性”。
@ServicepublicclassAppointmentService{@AutowiredprivateRedisTemplate<String,String>redisTemplate;@Transactional(rollbackFor=Exception.class)publicResultmakeAppointment(LongscheduleId,LongpatientId){// 1. 获取分布式锁(防止并发)StringlockKey="appointment_lock:"+scheduleId;StringlockValue=UUID.randomUUID().toString();booleanlocked=false;try{locked=redisTemplate.opsForValue().setIfAbsent(lockKey,lockValue,30,TimeUnit.SECONDS);if(!locked){returnResult.error("系统繁忙,请稍后再试");}// 2. 检查号源(使用悲观锁或乐观锁)DoctorScheduleschedule=scheduleMapper.selectForUpdate(scheduleId);if(schedule==null||schedule.getIsCancelled()==1){returnResult.error("该号源已停诊");}if(schedule.getRemainingCount()<=0){returnResult.error("号源已抢完");}// 3. 检查时间冲突(同一患者同一时间段只能有一个预约)booleanhasConflict=checkTimeConflict(patientId,schedule.getScheduleDate(),schedule.getTimeSlot());if(hasConflict){returnResult.error("该时间段已有其他预约");}// 4. 扣减号源introws=scheduleMapper.decreaseRemainingCount(scheduleId,schedule.getVersion());if(rows==0){returnResult.error("号源不足,请重新选择");}// 5. 创建订单Appointmentappointment=newAppointment();appointment.setOrderNo(generateOrderNo());appointment.setScheduleId(scheduleId);appointment.setPatientId(patientId);appointment.setStatus(AppointmentStatus.PENDING_PAYMENT.getCode());appointmentMapper.insert(appointment);// 6. 设置支付超时(15分钟)redisTemplate.opsForValue().set("appointment_pay_timeout:"+appointment.getId(),"1",15,TimeUnit.MINUTES);returnResult.success("预约成功,请在15分钟内支付",appointment);}finally{// 释放锁if(locked){if(lockValue.equals(redisTemplate.opsForValue().get(lockKey))){redisTemplate.delete(lockKey);}}}}}@ComponentpublicclassAppointmentTimeoutTask{@Scheduled(cron="0 */1 * * * ?")// 每分钟执行一次publicvoidhandleTimeoutAppointments(){// 1. 查询超时未支付的订单(创建时间超过15分钟)List<Appointment>timeoutList=appointmentMapper.selectTimeoutList();for(Appointmentappointment:timeoutList){try{// 2. 释放号源scheduleMapper.increaseRemainingCount(appointment.getScheduleId());// 3. 更新订单状态appointment.setStatus(AppointmentStatus.EXPIRED.getCode());appointmentMapper.updateById(appointment);// 4. 可选:发送通知(站内信/短信)notifyPatient(appointment.getPatientId(),"您的预约已超时取消");}catch(Exceptione){log.error("处理超时订单失败:{}",appointment.getId(),e);}}}}医生列表页:
排班选择页:
订单确认页:
医疗系统对权限要求严格,不同角色数据隔离。
@RestController@RequestMapping("/api/appointment")publicclassAppointmentController{// 患者只能看自己的预约@GetMapping("/my")@PreAuthorize("hasRole('PATIENT')")publicList<Appointment>getMyAppointments(){LongpatientId=getCurrentPatientId();returnappointmentService.findByPatientId(patientId);}// 医生只能看自己的排班预约@GetMapping("/doctor/my")@PreAuthorize("hasRole('DOCTOR')")publicList<Appointment>getDoctorAppointments(){LongdoctorId=getCurrentDoctorId();returnappointmentService.findByDoctorId(doctorId);}}@ComponentpublicclassDataEncryptor{privatestaticfinalStringKEY="your-secret-key-16bytes";// 身份证号加密存储publicStringencryptIdCard(StringidCard){try{Ciphercipher=Cipher.getInstance("AES/ECB/PKCS5Padding");SecretKeySpeckeySpec=newSecretKeySpec(KEY.getBytes(),"AES");cipher.init(Cipher.ENCRYPT_MODE,keySpec);byte[]encrypted=cipher.doFinal(idCard.getBytes());returnBase64.getEncoder().encodeToString(encrypted);}catch(Exceptione){thrownewRuntimeException("加密失败",e);}}// 查询时解密publicStringdecryptIdCard(Stringencrypted){// 解密逻辑}}挂号系统必须重点测试并发场景和异常流程。
| 测试场景 | 并发数 | 预期结果 | 测试工具 |
|---|---|---|---|
| 同一号源多人抢 | 100人同时抢10个号 | 只有10人成功,90人失败 | JMeter |
| 支付超时释放 | 创建订单不支付 | 15分钟后自动取消,号源释放 | 手动测试 |
| 医生临时停诊 | 停诊后患者预约 | 所有预约自动取消,退款 | 手动测试 |
@TestpublicvoidtestConcurrentAppointment(){// 模拟100个线程同时抢号ExecutorServiceexecutor=Executors.newFixedThreadPool(100);CountDownLatchlatch=newCountDownLatch(100);AtomicIntegersuccessCount=newAtomicInteger(0);for(inti=0;i<100;i++){executor.submit(()->{try{Resultresult=appointmentService.makeAppointment(scheduleId,patientId);if(result.isSuccess()){successCount.incrementAndGet();}}finally{latch.countDown();}});}latch.await();// 验证:成功数 <= 号源总数assertTrue(successCount.get()<=totalCount);}医疗系统答辩要突出业务逻辑的严谨性和数据的安全性。
需要完整的挂号系统源码、JMeter并发测试脚本、医疗数据脱敏方案的同学,评论区留言“医院挂号”。遇到并发控制、时间冲突检测等问题也可以提问。
记住:医疗系统的核心是可靠和安全,不是功能的多少。
点赞收藏,做严谨的医疗系统毕设!祝大家顺利通过答辩!🏥