简介这是一套基于SpringbootVue的在线问诊系统项目源码主要面向计算机相关专业学生可用作毕业设计、课程设计或期末大作业也适合Java学习者进行前后端分离实战练习。系统结合Springboot后端与Vue前端围绕患者在线咨询、医生接诊等典型场景完整呈现从需求分析、数据库设计到接口开发与页面联调的过程。资源共598个文件压缩包16.09MB核心类型包括116个java源码、90个vue组件、63个js脚本、15个xml配置、15个css样式以及sql数据库脚本附带svg图标、jpg/png图片和项目运行所需的jar、yml、bat构建脚本等覆盖开发、配置、部署各环节。目前已有72人学习下载适合作为可直接运行的毕设范本。配套资料包含开发说明文档、部署视频和代码讲解视频可帮助快速理解项目架构同时提供数据库脚本、构建与启动脚本简化环境搭建和本地调试流程便于在此基础上扩展功能或二次开发。1. 在线问诊系统的设计与实现先从状态一致性说起医生号源被两个患者在同一个十毫秒里同时挂到问诊单状态被接口直接改到“已完成”病历表和订单表的主键对不上——基于 Spring Boot 的在线问诊系统设计与实现真正藏坑的地方不在聊天推送而在排班、问诊单、病历这三张表的流转。很多人把进度做成“能发消息”验收时却卡在号源不超卖和状态回退这两个点上。这个系统的典型使用场景是医院信息科实习、毕业设计或小诊所自建预约入口。流程并不大但闭环完整医生排班、患者选号、下单支付、进入候诊、医生写病历、结束问诊。下面按 Spring Boot 的分层工程结构把表结构、状态机、权限控制和部署验证四条线拆开给出的代码和参数可以直接改包名后跑起来。2. 表结构设计把在线问诊系统的数据边界先画出来2.1 用户表拆分一张基础账号表加两张业务资料表在线问诊系统的用户有患者、医生和后台管理员三种角色。最常见的坏设计是建一张大用户表把科室、职称、病史、过敏史全部塞进同一行最后一半字段空着。正确做法是拆成基础账号表和业务扩展表。-- sys_user 只存登录凭证与角色 -- role_code 建议存字符串PATIENT / DOCTOR / ADMIN CREATE TABLE IF NOT EXISTS sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT BCrypt 加密后的密码, real_name VARCHAR(32) NOT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL, role_code VARCHAR(16) NOT NULL COMMENT PATIENT/DOCTOR/ADMIN, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统账号表;患者和医生各建一张资料表通过user_id与sys_user关联。这么拆的理由很直接问诊单、排班表引用的是doctor_id和patient_id不是sys_user.id。如果后续要加身份证号、紧急联系人、医保卡号只需要动资料表账号表不会因为需求扩展反复加列。2.2 三张核心业务表的字段参数清单问诊系统的业务主链路只有三张表doctor_schedule排班表、consultation_order问诊单、medical_record病历表。下面这张表是创建表时必填的字段基线。表名关键字段字段说明建议参数doctor_scheduleid, doctor_id排班主键与医生IDdoctor_id 建普通索引doctor_schedulework_date, time_slot出诊日期与时段time_slot 用 TINYINT1上午 2下午 3晚间doctor_schedulereg_limit, reg_count总号数和已挂号数都设为 INTreg_count 必须小于 reg_limitconsultation_orderorder_no订单业务编号VARCHAR(32)建唯一索引由后端生成consultation_orderschedule_id, patient_id, doctor_id三方关联三个字段都建索引consultation_orderstatus状态机字段TINYINT0待支付 1待问诊 2问诊中 3已完成 4已取消consultation_ordervalid_flag逻辑有效标记TINYINT有效订单为1完成无效为NULLmedical_recordorder_id与问诊单一对一唯一索引medical_recordchief_complaint, diagnosis_result主诉与诊断VARCHAR(500) 或 TEXT注意valid_flag这个字段容易被忽略。同一个患者对同一排班不允许同时存在两笔有效订单但 MySQL 不支持部分索引直接用status做唯一索引会把已取消的订单也锁住。为了让唯一索引允许重复的已取消订单有效时存order_no失效时置 NULL。开头别嫌字段冗余它能兜住重复下单。2.3 用 spring.sql.init 让 Spring Boot 启动时自动建表MyBatis 本身没有“当表不存在自动建表”的能力但 Spring Boot 的 SQL 初始化机制可以配合schema.sql实现同样效果。把建表语句集中放到src/main/resources/db/schema.sql然后在application.yml中加两行配置spring: sql: init: mode: always schema-locations: classpath:db/schema.sql datasource: url: jdbc:mysql://localhost:3306/clinic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:root}配置里有三个参数值得说明。mode: always表示每次启动都执行 SQL适合开发期如果只想启动时建表、之后不再干预生产环境要改回never。schema-locations用classpath:前缀定位资源目录下的文件。serverTimezone是 MySQL 连接的老大难不写对的话时间字段会差 8 个小时。提示Spring Boot 2.5 起旧的spring.datasource.initialization-mode已废弃统一用spring.sql.init.*。如果你的项目从旧配置迁移只改参数名SQL 文件路径不用动。schema.sql里的每条建表语句都建议写IF NOT EXISTS。配合mode: always反复启动时第二次执行不会报“表已存在”的错。3. 核心业务流程挂号、候诊、写病历的状态机实现3.1 号源检查用 select ... for update而不是先查后改在线问诊系统最容易出事故的地方是号源超卖。两个患者同时操作时先查reg_count发现还剩一个号然后一起执行插入订单结果同一排班被下单两次。解决思路是让检查号源和占用号源变成原子操作。Transactional(rollbackFor Exception.class) Override public ConsultationOrder createOrder(CreateOrderRequest request) { // 加排他锁锁住排班行后到的请求在此处阻塞等待 DoctorSchedule schedule scheduleMapper.selectByIdForUpdate(request.getScheduleId()); if (schedule null) { throw new BizException(排班不存在); } if (schedule.getRegLimit() - schedule.getRegCount() 0) { throw new BizException(号源已满); } // 占用号源先扣减再插入订单保证排班行锁被持有到事务结束 scheduleMapper.increaseRegCount(schedule.getId()); ConsultationOrder order ConsultationOrder.builder() .orderNo(generateOrderNo(schedule.getId())) .patientId(request.getPatientId()) .doctorId(schedule.getDoctorId()) .scheduleId(schedule.getId()) .status(OrderStatus.WAIT_PAY) .validFlag(orderNo) .build(); orderMapper.insert(order); return order; }这段代码里真正起作用的是selectByIdForUpdate。它会对排班表的这一行加排他锁第二个请求只要事务没提交就会一直阻塞在查询上。第一个事务回滚时锁释放第二个请求读到的是扣减后的真实号源。用乐观锁也可以做但要处理版本号冲突后重试的逻辑在这个业务规模下没必要。对应 Mapper 里的两条 SQL 我通常会写死select idselectByIdForUpdate resultTypeDoctorSchedule SELECT * FROM doctor_schedule WHERE id #{id} FOR UPDATE /select update idincreaseRegCount UPDATE doctor_schedule SET reg_count reg_count 1 WHERE id #{id} /update事务回滚时increaseRegCount的扣减会一并回滚所以不需要手动补号。但这里有一个前提取消订单的方法必须跟着做reg_count - 1否则取消一单就永久少一个号。3.2 状态机防止跳步问诊单不能随便 UPDATE问诊单的状态流转如果写成任意可修改字段会出现“还在候诊就被改成已完成”的脏数据。状态机做法是把状态变更收口到一个方法里业务代码只允许执行动作不允许直接改状态字段。public OrderStatus doTransition(OrderStatus current, OrderAction action) { switch (current) { case WAIT_PAY: if (action OrderAction.CANCEL) return CANCELED; if (action OrderAction.PAY) return PAID; break; case PAID: if (action OrderAction.ENTER_CONSULT) return IN_CONSULTATION; break; case IN_CONSULTATION: if (action OrderAction.FINISH) return FINISHED; if (action OrderAction.MANUAL_CLOSE) return CANCELED; break; default: break; } throw new BizException(当前状态不允许执行该操作); }状态变更表如下整张表就是状态机的需求文档当前状态可执行动作目标状态触发时的附加操作WAIT_PAYcancelOrderCANCELED释放号源reg_count 减一WAIT_PAYpaySuccessPAID记录支付单号和支付时间PAIDenterConsultIN_CONSULTATION记录问诊开始时间IN_CONSULTATIONfinishRecordFINISHED写入病历记录IN_CONSULTATIONmanualCloseCANCELED医生患者协商后关闭释放号源这么设计后业务层只有五个动作入口杜绝了直接写consultationOrder.setStatus(3)再 update 的危险操作。每个动作方法内部再加角色判断就形成了完整闭环。3.3 完成问诊时同步写病历病历与问诊单是一对一关系。业务上常见的错误是两个接口分开调先改订单状态为完成再调另一个接口存病历。一旦第二个接口失败订单已完成但病历缺失。正确做法是把状态流转和病历插入放在同一个事务里。Transactional(rollbackFor Exception.class) public void finishConsultation(FinishConsultationCommand command) { ConsultationOrder order orderMapper.selectById(command.getOrderId()); // 先做状态流转非法状态在这里直接抛异常 OrderStatus target doTransition(order.getStatus(), OrderAction.FINISH); order.setStatus(target); order.setValidFlag(null); orderMapper.updateById(order); MedicalRecord record MedicalRecord.builder() .orderId(order.getId()) .chiefComplaint(command.getChiefComplaint()) .presentIllness(command.getPresentIllness()) .diagnosisResult(command.getDiagnosisResult()) .advice(command.getAdvice()) .build(); medicalRecordMapper.insert(record); }这段代码里有两个细节。第一validFlag置 NULL 释放“该患者对该排班的有效订单”唯一约束允许同一个患者下次重新挂号同一排班。第二判定问诊是否完成看的不只是状态值而是事务提交后medical_record里有没有对应记录。4. 鉴权与并发防护用 Spring Security JWT 守住管方和患者4.1 用 requestMatchers 区分患者端与医生端接口在线问诊系统必须保证医生不能操作患者的挂号接口患者不能写病历。我在 Spring Boot 项目里用 Spring Security 6 的 Lambda 风格 DSL 配置。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/public/**).permitAll() .requestMatchers(/api/patient/**).hasRole(PATIENT) .requestMatchers(/api/doctor/**).hasRole(DOCTOR) .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }requestMatchers是 Spring Security 5.6 之后推荐写法。很多旧教程里的antMatchers在 Spring Security 6 中已被删除新项目如果从网上抄老代码启动时会直接报NoSuchMethodError。2.7 及以下版本用authorizeRequests()写法3.x 一律用authorizeHttpRequests()。hasRole(DOCTOR)会自动加前缀ROLE_所以 SysUser 里存的角色名必须是ROLE_DOCTOR或者在构建 Authentication 时手动加上前缀。最常见的对不齐问题就出在这里。4.2 JWT 生成的关键参数说明JWT 鉴权在在线问诊系统里有几个参数需要定死不能偷懒参数建议值说明签名算法HS256JJWT 默认支持无需额外依赖密钥长度至少 32 字节小于 32 字节会报 WeakKeyExceptiontoken 有效期2 小时问诊过程通常小于 2 小时刷新策略前端静默重新登录本项目不做单独 refresh token简化部署JwtAuthenticationFilter 的写法是所有安全功能的落点它从请求头取Authorization解析 token然后把用户 ID 和角色塞进 SecurityContext。public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { String userId JwtUtil.getUserId(token); String role JwtUtil.getRole(token); if (userId ! null) { UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority(ROLE_ role))); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }UsernamePasswordAuthenticationToken的第三个参数是权限列表必须传ROLE_前缀。业务代码里后续用PreAuthorize(hasRole(DOCTOR))时验证的就是这个前缀后的值。4.3 用 Redis 的 SETNX 做防重复提交在线问诊系统中患者双击“支付确认”按钮是常规操作。JWT 能解决“谁在操作”解决不了“同一请求被提交两次”。用 Redis 的setIfAbsent实现一个轻量幂等锁。Aspect Component public class IdempotentAspect { private final StringRedisTemplate redisTemplate; public IdempotentAspect(StringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; } Around(annotation(idempotent)) public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { String requestId RequestContextHolder.getRequestContext().getRequestId(); String key idem:order:create: requestId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(key, 1, Duration.ofSeconds(idempotent.expireSeconds())); if (Boolean.FALSE.equals(locked)) { throw new BizException(请勿重复提交); } return joinPoint.proceed(); } }setIfAbsent只有第一次调用返回 true第二次请求在过期时间内会拿 false直接抛异常。锁的过期时间不建议超过 5 秒业务接口正常执行都在百毫秒级太长的锁会误伤用户的合法重试。这里刻意没有在方法结束后删除 key目的是让锁自然过期避免并发下先删锁导致第二个请求提前进入。5. 上线前过一遍这三个检查配置、提醒任务和链路验证5.1 把数据库密码从 yml 明文改为环境变量覆盖不要求改造成复杂的加密组件但至少要避免把生产库密码写死在application.yml里提交到代码仓库。Spring Boot 原生支持环境变量覆盖spring: datasource: password: ${DB_PASSWORD:changeit}启动时传入DB_PASSWORD你真正的密码即可。changeit只是本地开发兜底值生产环境不传环境变量密码就是错的。5.2 问诊前 30 分钟提醒任务的分布式收敛Scheduled 做提醒任务是最省事的实现但要注意多实例部署会重复触发。加一把 Redis 锁Scheduled(cron 0 */1 * * * ?) public void remindPatients() { String lockKey lock:consult-remind; Boolean gotLock redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofMinutes(1)); if (!Boolean.TRUE.equals(gotLock)) { return; } // 查询 30 分钟后开始且状态为待问诊的订单 // 给患者发短信或站内信 ListConsultationOrder orders orderMapper .findByStartTimeBetweenAndStatus(LocalDateTime.now().plusMinutes(30), LocalDateTime.now().plusMinutes(31), OrderStatus.PAID); notifyService.notifyPatient(orders); }cron 表达式0 */1 * * * ?表示每秒 0 分、每分钟执行一次。Redis 锁过期时间设为 1 分钟锁的粒度是整个提醒任务保证同一分钟只有一个实例去扫库。5.3 用一行 curl 验证鉴权是否真的生效启动项目后建议把下面三个请求保存成一个 smoke test 脚本每次改完安全配置跑一遍# 无 token 访问患者接口期望返回 401 curl -i http://localhost:8080/api/patient/orders # 使用患者 token 访问医生写病历接口期望返回 403 curl -i -H Authorization: Bearer $PATIENT_TOKEN http://localhost:8080/api/doctor/record # 正确携带医生 token期望返回 200 curl -i -H Authorization: Bearer $DOCTOR_TOKEN http://localhost:8080/api/doctor/record第一条验证 JWT 过滤器有没有生效第二条验证角色权限第三条验证正常链路没有被误伤。再把第 5.2 节的定时提醒日志打开看打印的订单数量是否和select count(*) from consultation_order where status1 and start_time between now()30min and now()31min的结果一致这套基于 Spring Boot 的在线问诊系统才算真的闭合。本文还有配套的精品资源点击获取