SpringBoot+Vue中医院问诊系统:课设毕设实战指南
发布时间:2026/10/7 18:23:31 锦皓数字建站

简介中医院问诊系统的设计与实现基于SpringBootVue前后端分离架构完成压缩包内包含完整可运行源码与数据库脚本适合计算机类专业学生用于课程设计或毕业设计也可供需要快速搭建同类业务系统的人员参考。项目采用Java语言、SpringBoot后端框架与Vue前端框架前端围绕问诊流程、科室挂号、用户管理等常见业务模块展开后端提供对应接口服务与数据持久化逻辑整体目录结构清晰便于二次开发。资源包共857个文件大小约23.37MB其中java文件220个对应后端核心代码vue文件158个覆盖前端主要页面组件另有63个js脚本、15个css样式文件及大量svg、jpg等静态资源并包含SQL数据库脚本和bat启动/构建脚本方便在MySQL 5.7以上环境配合Maven直接部署运行。开发环境推荐JDK1.8与idea或eclipse压缩包内附install、run、build等批处理命令可帮助快速完成环境准备、项目启动与打包构建适合作为课设答辩演示、毕设系统展示或SpringBoot与Vue整合学习的落地样例。当前已有78人学习下载说明该项目对同类课程设计具有一定参考价值。1. 一个能当场讲清业务闭环的课设项目SpringBootVue中医院问诊系统课设答辩时老师最常问的一句话是“你这个系统除了能登录到底干了什么”基于SpringBootVue的中医院问诊系统就属于那种能当场讲清业务闭环的项目患者在线提交主诉、完成预约医生接诊后按望闻问切录入四诊信息系统根据辨证结果辅助生成处方患者端能查看医嘱和用药清单。前后端分离SpringBoot提供接口Vue负责页面源码可运行适合本科课设和毕设直接起步或二次开发。这套系统的核心难点不在CRUD而在三件事问诊记录怎么建模、问诊状态怎么流转、处方数据结构怎么存才合理。想清楚这三个问题写代码只是体力活。2. 从挂号到辨证中医院问诊系统的数据建模与建表脚本2.1 拆解问诊业务六个模块把中医流程说清楚先想清楚业务边界。中医院问诊系统和普通医院挂号系统最大的差别在环节分岔普通门诊是挂号→看诊→开药中医问诊里看诊被替换成“四诊采集→辨证分型→治法确立→开方”。很多课设源码在这一块模糊处理结果答辩时被问“你的辨证结果是怎么来的、存到哪了”就答不上来。我的做法是把业务拆成六个独立模块用户患者/医生/管理员三种角色、科室与医生、预约问诊、问诊记录四诊信息、辨证处方、基础字典。这里字典表是关键——证候分型如肝郁气滞、脾虚湿盛不能做成用户输入的字符串要进字典表这样处方才能和辨证结果真正挂钩。六个模块映射到六张核心基础表sys_user、medical_doctor、appointment、diagnosis_record、prescription、prescription_item外围再挂科室表和证候字典表。整个MySQL库的表结构决定了后续所有Controller和页面怎么组织所以第一步必须把表和字段定死不要等代码写了一半再回头改表。2.2 建表脚本四诊信息和处方明细不能想当然核心表结构我直接给出建表脚本其中四诊字段用TEXT存原文、证候用字典code存储这两条设计是后面接口实现不返工的前提。-- 用户表三种角色用role_type区分 CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(30) COMMENT 真实姓名, phone VARCHAR(20), role_type TINYINT NOT NULL COMMENT 0患者 1医生 2管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 问诊记录表四诊信息用TEXT存原文辨证结果引用字典code CREATE TABLE diagnosis_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, appointment_id BIGINT NOT NULL COMMENT 关联预约, patient_id BIGINT NOT NULL COMMENT 患者ID, doctor_id BIGINT NOT NULL COMMENT 接诊医生ID, inquiry_text TEXT COMMENT 问诊对话/主诉原文, look_sign TEXT COMMENT 望诊面色、舌质、舌苔, listen_sign TEXT COMMENT 闻诊声息、气味, ask_sign TEXT COMMENT 问诊寒热、汗、二便, pulse_sign TEXT COMMENT 切诊脉象描述, syndrome_type VARCHAR(30) COMMENT 证候分型对应字典表code, treatment_principle VARCHAR(255) COMMENT 治法, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待接诊 1问诊中 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 处方明细表一症一方明细行存草药与克数 CREATE TABLE prescription_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, prescription_id BIGINT NOT NULL, herb_name VARCHAR(100) NOT NULL COMMENT 中药名, dosage VARCHAR(50) COMMENT 剂量如10g、3枚, note VARCHAR(255) COMMENT 煎服法备注 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明问诊主表把四诊拆成四个TEXT字段而不是塞一个大JSON理由是答辩时能直接按字段展示“舌苔薄白”“脉弦细”这类原始记录同时保留inquiry_text存医患对话全文作为患者端历史可查的主诉原文。syndrome_type存字典表的code而不是中文名避免同一证候出现“肝郁气滞”和“肝郁气滞型”两种写法后面做统计时不用清洗数据。每个字段都带COMMENT课上演示和老师翻数据库表结构时一目了然。参数说明status字段就是问诊状态机0到3四个值这是全程最容易改崩的地方后面避坑章节会单独展开。password这里不存明文用BCrypt加密后再入库配合Spring Security或jBCrypt工具类都行。角色字段用TINYINT而不是字符串查询比较更快也避免大小写问题。2.3 设计分岔点辨证放在规则里还是放在人工很多同学会纠结一个问题要不要做自动辨证功能我的建议是课设做到“半自动”就收敛。做法是证候字典表里存一列症状关键词组选完主诉、舌象、脉象后后端做一个简单的模糊匹配返回几个候选证候由医生确认或修正最终只提交确认后的syndrome_type。这样既显得有算法逻辑又不会陷入中医知识库这个无底洞——想靠代码搞定真正的中医辨证那是甲方几百万项目的事课设不背这个锅。表名承担职责关键字段与中医流程的对应sys_user登录与角色username, password, role_type患者/医生/管理员入口medical_doctor医生执业信息user_id, title, department_id医生资质展示appointment预约与排期patient_id, doctor_id, time_slot, status问诊前环节diagnosis_record四诊与辨证四诊TEXT, syndrome_type, status问诊核心环节prescription处方头record_id, doctor_id, total_amount辨证后开方prescription_item处方明细prescription_id, herb_name, dosage具体用药表设计是答辩的第一道防线。很多源码包拿到手先跑起来再说但老师往往会直接看数据库脚本六张表能讲清楚整个业务链条就站得住。中医问诊的信息化核心就是把“望闻问切”变成可存储、可追溯、可统计的结构化数据这套建模就是这个思路的直接体现。3. SpringBoot后端把问诊流程做成状态机与接口闭环3.1 项目结构与配置用最稳的骨架起步课设项目不要引入花哨组件SpringBoot 2.7.x MyBatis-Plus 3.5.x MySQL 8.0是这几年最稳的组合。项目结构按标准分包controller、service、mapper、entity、config、common。config里放三个核心BeanCORS配置、MyBatis-Plus分页插件、JWT拦截器。这个springboot项目结构不要自己发明照着主流分层写后面查问题、找资料都容易。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/tcm_clinic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true逻辑说明连接串里的useUnicode和characterEncoding控制中文写入serverTimezone定义时区避免时间字段差8小时jackson.date-format统一接口返回的时间格式前端不用再手动格式化字符串。MyBatis-Plus的map-underscore-to-camel-case配置让数据库的snake_case自动映射到实体的camelCase少写一堆resultMap。参数说明端口可以按演示环境改成8081但要同步改前端axios的baseURL。管理员初始密码用BCrypt加密后的密文写进初始化脚本不要放明文否则老师看application.yml时会觉得你安全意识太弱。3.2 问诊状态机Service层把流程卡死问诊系统最容易翻车的地方是页面交互患者提交预约后医生还没接诊患者就开始填写四诊或者医生A接诊后医生B也能改记录。问题根源是状态机没有在Service层卡住。我定三条规则待接诊状态只能由医生操作流转为问诊中同时锁定doctor_id问诊中状态允许修改四诊和药方但只能由当前接诊医生操作只有完成辨证和处方的记录才能流转为已完成。这三条规则写进同一个原子方法里。public Boolean updateDiagnosis(DiagnosisRecord record, Long operatorId) { DiagnosisRecord dbRecord diagnosisRecordMapper.selectById(record.getId()); if (dbRecord null) { throw new ServiceException(问诊记录不存在); } Integer status dbRecord.getStatus(); Long doctorId dbRecord.getDoctorId(); // 状态 0只能医生接诊接诊时写入doctorId并流转到1 if (status 0) { if (!doctorService.isDoctor(operatorId)) { throw new ServiceException(只有医生可以接诊); } dbRecord.setDoctorId(operatorId); dbRecord.setStatus(1); diagnosisRecordMapper.updateById(dbRecord); return true; } // 状态 1锁定当前接诊医生允许更新四诊、证候、处方 if (status 1) { if (!doctorId.equals(operatorId)) { throw new ServiceException(该问诊已被其他医生接诊); } record.setStatus(1); int rows diagnosisRecordMapper.updateById(record); if (rows 0) { throw new ServiceException(更新失败请刷新后重试); } return true; } throw new ServiceException(当前状态不允许修改); }逻辑说明updateById之前先读数据库里当前的状态和doctorId以数据库值为准而不是信任前端传过来的对象。这一步能杜绝并发条件下两个医生同时接诊同一条记录的问题。状态0分支只认doctorService.isDoctor(operatorId)如果接口拿前端传的roleType当信任凭据等于登录功能白做——攻击者改个请求体就能冒充医生。参数说明operatorId从哪里来从JWT令牌里解析登录后每个请求都在拦截器里拿到userId放进request属性Controller再取出传给Service。全程不要用前端传的patientId或doctorId覆盖这个值。这一步是权限链的根基我在课设答辩时被老师专门追问过。3.3 JWT登录与拦截器把权限链讲清楚登录接口统一走/sys/login成功后签发JWT过期时间设30分钟前端把token存在localStorage。后端拦截器拦截/api/**放行/api/login和字典查询接口其余请求校验Authorization头。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; // 预检请求放行否则跨域请求第一次就挂 } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new AuthException(未登录); } Long userId JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, userId); return true; } }逻辑说明OPTIONS预检请求必须放行否则前端axios跨域时会先失败。token解析出的userId放进request属性后续Controller通过RequestAttribute(userId)就能拿到当前操作人。这里没有每次查数据库判断登录态是否过期对课设来说够用如果系统要真正上线再加Redis做服务端token续期和管理。参数说明JWT工具底层用jjwt或hutool都行密钥写死在配置里开发阶段没问题。token存储选localStorage还是cookies我一般选localStorage因为Vue路由守卫里判断登录跳转时可以直接读取不需要额外引入cookie库。这里其实已经暗示下一个章节前端才是用户直接面对的部分路由和页面交互必须跟后端的角色模型严格对齐。4. Vue前端从路由守卫到问诊对话页的落地4.1 页面架构与路由权限先搭好骨架前端用Vue 2.6 Element UI axios这套组合和SpringBoot后端的适配成本最低。页面路由分成三条线患者端预约问诊、问诊记录、我的处方、医生端待接诊列表、四诊录入、开方、管理端用户管理、科室管理、数据统计。路由配置里用meta.roles声明谁能访问对应页面。import Vue from vue; import Router from vue-router; import store from /store; Vue.use(Router); const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /patient, component: () import(/layout/PatientLayout.vue), meta: { roles: [PATIENT] }, children: [ { path: appointment, component: () import(/views/patient/Appointment.vue) }, { path: records, component: () import(/views/patient/Records.vue) } ] }, { path: /doctor, component: () import(/layout/DoctorLayout.vue), meta: { roles: [DOCTOR] }, children: [ { path: pending, component: () import(/views/doctor/PendingList.vue) }, { path: diagnosis/:id, component: () import(/views/doctor/Diagnosis.vue), props: true } ] } ]; const router new Router({ mode: hash, routes }); router.beforeEach((to, from, next) { const token store.state.token; if (to.path /login) return next(); if (!token) return next(/login); const role store.state.role; if (to.meta.roles !to.meta.roles.includes(role)) { return next(/403); } next(); }); export default router;逻辑说明路由模式用了hash而没选history。原因很朴素hash模式下前端打包后扔进SpringBoot的static目录刷新不会404不需要额外配后端转发课设交付最省事。vue路由守卫两层筛选先判断token是否存在再判断角色是否匹配meta.roles这样不同角色连页面入口都看不到而不是进入页面后才弹“无权限”。diagnosis/:id这种带参数路由就是医生从待接诊列表点进来的入口props: true让页面通过props直接拿到id比在组件里写this.$route.params更干净。参数说明子路由children配合layout组件实现左侧菜单栏、右侧内容区的后台布局每个页面不用重复写整体框架。store是Vuex实例token和role在登录成功后commit进去刷新页面时再从localStorage恢复保证路由守卫不会误判。4.2 axios封装token注入和错误拦截一步到位前端每一个请求都要带token后端拦截器才会放行。用axios拦截器统一处理而不是在每个请求方法里手动加header否则几十个接口改到怀疑人生。import axios from axios; const service axios.create({ baseURL: process.env.VUE_APP_API || http://localhost:8080/api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 0) { Message.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg)); } return res.data; }, error { if (error.response error.response.status 401) { router.push(/login); } Message.error(网络异常请检查后端服务); return Promise.reject(error); } ); export default service;逻辑说明请求拦截器负责加tokenresponse拦截器统一解包。后端返回格式约定是{code, msg, data}code为0表示成功这样前端页面拿到的直接是业务数据而不是再包一层对象。401时自动踢回登录页不会出现“页面白屏但控制台报错”这种答辩时的尴尬场景。timeout设10秒防止后端某个慢查询把页面一直挂着转圈。参数说明baseURL从环境变量读取开发环境指向http://localhost:8080/api打包前改成和后端同域的/api。这样就能配合后面“打包进SpringBoot”的交付方式生产环境完全不走跨域。另外process.env.VUE_APP_API需要在.env.development和.env.production两个文件里分别定义这是Vue CLI约定别直接写死。4.3 问诊对话页把四诊录入做成医生顺手的样子医生接诊后面对的不是一个普通表单而是一个“对话式问诊”工作台左侧是患者提交的主诉和症状标签右侧是四诊录入区下方有快捷问句栏中医常见问题怕冷还是怕热、汗多不多、口渴喜不喜热饮一键填入。这个页面是整套系统里交互最复杂的地方也是演示时最能出效果的页面。template div classdiagnosis-container el-descriptions :modelrecord title患者主诉 :column1 border el-descriptions-item label主诉{{ record.inquiryText }}/el-descriptions-item /el-descriptions el-form :modelform label-width80px el-input typetextarea v-modelform.lookSign placeholder望诊面色、舌质、舌苔 / el-input typetextarea v-modelform.pulseSign placeholder切诊脉象描述如弦细 / el-select v-modelform.syndromeType placeholder选择证候分型 filterable el-option v-foritem in syndromes :keyitem.code :labelitem.name :valueitem.code / /el-select el-button typeprimary clicksubmitDiagnosis提交辨证与四诊/el-button /el-form /div /template script export default { data() { return { form: { lookSign: , pulseSign: , syndromeType: }, syndromes: [] }; }, created() { this.fetchDetail(); this.fetchSyndromes(); }, methods: { fetchDetail() { api.get(/diagnosis/${this.$route.params.id}).then(data { this.record data; }); }, submitDiagnosis() { api.post(/diagnosis/update, { ...this.form, id: this.$route.params.id }) .then(() this.$message.success(已保存)); } } }; /script逻辑说明el-select的filterable属性让证候分型支持搜索字段存的是code、展示的是name和第2章字典表设计对齐。created钩子里并行拉取问诊详情和证候字典v-model双向绑定的是form对象提交时把form和id拼成一个对象发给后端update接口后端会严格走状态机校验当前医生是否匹配。快捷问句栏没有写进代码块里是一个数组遍历渲染的按钮集合点击后把问句文本追加到askSign字段属于纯前端交互几分钟能加完。参数说明textarea的placeholder只是提示真正医生录入时文本会很长所以后端字段必须是TEXT而不是varchar(255)这是很多网上下载的源码翻车的高发点。四诊字段我故意没有用动态表格而是四个独立textarea答辩演示时直观、截屏也清楚后端实体类字段能一一对上。5. 部署与交付避坑5个让源码“可运行”失效的经典问题5.1 SpringBoot版本太高导致启动即失败现象从网上下载源码包pom里写的是SpringBoot 3.x本地装的是JDK 8一启动就报UnsupportedClassVersionError。原因SpringBoot 3.0起强制要求JDK 17且javax.servlet包改成了jakarta.servlet老课设里大量import javax.*的代码全部编译不过。这是一个真实的“springboot版本太高”翻车现场下载源码时版本选择比功能还重要。解决课设阶段统一用SpringBoot 2.7.x JDK 8 MyBatis-Plus 3.5.x组合。如果源码已经用了3.x有两条路降级到2.7并把Jakarta改回javax或者本地装JDK 17并把javax全部替换成jakarta。我建议走降级路线因为网上检索到的排错资料里SpringBoot 2.7的答案最多真遇到问题好搜。改pom后记得mvn clean再重新编译别增量构建直接跑旧class文件。5.2 前后端联调跨域登录成功但业务请求全军覆没现象前端页面能打开输入账号密码点击登录Network里login接口200但后续所有业务请求都报CORS error。原因前端开发服务器在localhost:8081后端接口在8080axios发出的跨域请求在后端没有被允许。最常见漏网之鱼是OPTIONS预检请求没放行后端只处理了GET和POSTOPTIONS直接返回405。解决后端加一个CORS配置类用WebMvcConfigurer注册跨域映射。注意两个细节allowCredentials(true)时不能配allowedOrigins()必须用allowedOriginPatterns()allowedMethods要写全POST、GET、PUT、DELETE、OPTIONS少一个都不行。或者开发期给前端开代理vue.config.js里devServer.proxy把/api转发到8080。二选一别都配置导致请求绕来绕去。5.3 刷新页面就404路由模式的经典陷阱现象前端打包后丢进SpringBoot的static目录直接访问http://localhost:8080/index.html能打开点菜单跳转也正常但按F5刷新当前路由时直接404。原因Vue Router开了history模式前端路由是纯前端状态后端收到/patient/records路径找不到对应Controller就抛出404。hash模式没有这个问题因为带#号的部分不会发到服务端。解决最省事的方法是开局就选hash模式URL里带#刷新永远请求index.html。如果坚持history模式后端要写一个转发Controller把所有非静态资源路径转发到/index.html但这步对课设来说纯属增加工作量不推荐。看别人源码时先看router实例里mode写的是什么再决定要不要配后端转发。5.4 时间错乱8小时问诊时间显示成前一天现象患者端问诊记录里的时间比本地时间慢了8小时或者数据库里存的时间是对的但接口返回的数据格式带了字母T。原因MySQL连接串没配serverTimezoneJVM默认取系统时区数据库会话取UTC两边的时区一交错就出现8小时差。jackson序列化时如果没有指定date-formatLocalDateTime会按ISO格式输出成“2024-06-01T10:30:00”。解决数据库连接串带上serverTimezoneAsia/ShanghaiSpringBoot配置里加spring.jackson.time-zoneAsia/Shanghai。第3章的application.yml里已经写好直接抄就行。这个坑特别隐蔽因为本地测试时有时差正好被系统默认时区掩盖换一台机器部署就现原形。5.5 数据库中文乱码与emoji导致保存失败现象建表后插入中文变成“???”一串问号患者主诉里带emoji表情时整条记录保存报错。原因表或库的字符集是latin1或utf8。latin1根本不支持中文utf8在MySQL里最多3个字节存不了emoji这种4字节字符。很多图形化建表工具默认字符集是utf8当时能建表录数据才暴露问题。解决建库语句用DEFAULT CHARSETutf8mb4连接串加characterEncodingutf8。注意改库字符集后已存在的表也要逐个ALTER转换成utf8mb4否则新老表的关联查询拼接处还是会出现乱码。6. 交付前最后一步让源码从“能跑”到“能演示”角色和数据的预置是演示效果的分水岭。我习惯在系统里预置三个账号患者zhangsan、医生wangjing、管理员admin外加两条历史问诊记录和一张已经开好的处方。后端启动时用CommandLineRunner自动初始化脚本里判断sys_user表记录数大于0就跳过避免每次重启重复写入演示数据。Component public class DataInitializer implements CommandLineRunner { Autowired private UserService userService; Override public void run(String... args) throws Exception { if (userService.count() 0) return; User admin new User(); admin.setUsername(admin); admin.setPassword(BCrypt.hashpw(admin123, BCrypt.gensalt())); admin.setRoleType(2); userService.save(admin); User doctor new User(); doctor.setUsername(wangjing); doctor.setPassword(BCrypt.hashpw(123456, BCrypt.gensalt())); doctor.setRoleType(1); userService.save(doctor); } }启动完成后老师的操作路径就是完整演示链用wangjing登录医生端从待接诊列表接一条新记录填写四诊选择一个证候分型开一张处方退出登录用zhangsan登录患者端查看这条问诊记录的处方详情。这条链路正好把appointment、diagnosis_record、prescription、prescription_item这四张核心表都走了一遍演示完就能回答数据库设计问题。最后再讲一个交付细节前端打包后把dist目录里的文件复制到SpringBoot的src/main/resources/static下重新打包成单个jar。这样交付时只有一个jar包加一个sql脚本老师拷走zip后只要本机有JDK和MySQL一条命令java -jar就能启动完整系统不需要同时开两个终端。对应热词就是“vue打包放进springboot中”这也是这个交付方式在课设场景里最受欢迎的原因。至于“vue项目源码怎么发给别人”这类问题我通常会把数据库初始化脚本和jar包放在同一层目录再用一份README写明JDK版本和MySQL密码。以前我做过一个没有状态机校验的问诊系统两个医生能同时改同一条问诊记录答辩时被老师连续追问“你凭什么保证数据不被覆盖”那次之后我凡是涉及状态流转的操作都在Service层先把状态和操作人校验写死再放行。这个教训也写进了上面第3章的代码里你以后自己接项目时这个习惯能帮你躲开很多“线上数据被改乱”的雷。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。