拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于SpringBoot的中医诊所预约挂号系统设计与实现

1. 项目定位为什么是中医诊所而非医院很多人第一次看到这类题目第一反应是不就是个挂号系统吗换个皮肤而已。如果你也这么想那这个项目基本白做了。我之所以强调这是中医诊所而非三甲医院是因为两者的业务模型差异极大直接决定了系统的表结构、流程设计和代码实现方向。综合医院的预约挂号核心是科室—医生—号源三层关系医生属于科室患者选择科室再选择医生号源按时段批量放出去爽约率、加号、退号基本靠独立子系统处理患者身份靠就诊卡或医保卡绑定。而中医诊所的典型场景是什么一个诊所往往只有几位固定大夫每位大夫一周出诊时间是相对固定的患者复诊率高且很大一部分老患者是认人不认店——他们挂的不是科室是某个明确的大夫。更关键的是中医讲究首诊留档初诊时大夫要写详细的四诊信息望闻问切、舌苔脉象记录这些信息是后续复诊开方的重要参考必须随患者档案长期保存而不是像综合医院那样每次挂号都重新建一个就诊事件。这就决定了你的数据模型不能直接套用网上那些医院预约挂号系统的表结构。网上能找到的开源项目十个里有八个是抄的同一套教学设计模板patient表、doctor表、appointment表、time_interval表完事了。但放到中医诊所场景至少要额外考虑几个问题患者和中医师之间是否存在长期绑定关系复诊时如何快速识别不同大夫的出诊时段是否支持个性化配置比如张大夫每周一三五上午李大夫只有周二下午预约之后医生能否在系统中看到这个患者的历史诊疗摘要我见过太多做这类课题的同学花了大把时间在抢票逻辑上——锁号、并发扣减、Redis队列——却完全没有处理中医诊所最核心的需求。其实你的用户量根本到不了需要Redis锁的程度一个MySQL事务加唯一索引就已经绰绰有余。把精力放在数据建模和业务流程的合理性上才是这个题目真正的得分点。所以这篇文章我会按照我实际做这个项目的思路来拆解从业务建模讲起再到核心预约流程的技术实现然后是权限和病历模块的处理方式最后是部署阶段那些踩过的坑。2. 技术选型SpringBoot为主体的架构考量技术选型这件事答案本身不复杂但背后的理由值得掰扯清楚。这个题目用的是SpringBoot那就从SpringBoot说起。2.1 为什么SpringBoot是这类项目的标准答案先别急着说因为学校要求或者因为网上教程多。SpringBoot能成为中小型管理系统的绝对主流是有技术上的实际原因的。第一自动配置大幅降低了基础设施接入成本。你需要数据库连接池、ORM框架、JSON序列化、Web容器在SpringBoot里就是一个starter依赖的事。对比传统Spring项目动辄几百行XML配置SpringBoot把约定优于配置贯彻到了极致。开发效率层面的差距对于三到五个月周期的毕设项目来说是决定性的。第二生态成熟度无人能比。安全认证有Spring Security接口文档有Swagger/knife4j持久层有MyBatis-Plus权限控制有Sa-Token这些都是被无数生产项目验证过的组件。你不需要自己造轮子组合起来就是一套稳定可靠的系统。做项目最怕的不是功能做不完而是在某个冷门框架的bug上卡两周。第三内置Tomcat打包即运行。这一点将直接关系到后面的远程部署。SpringBoot的spring-boot-maven-plugin可以把项目打成一个可执行的fat jar服务器上只需要装一个JDKjava -jar一条命令就能起服务。相比传统war包需要配置外部Tomcat、调整server.xml部署复杂度下降了一个量级。至于前端这个题目提到源码lw远程部署没限定前后端分离还是服务端渲染。我做的版本采用的是SpringBoot Vue 3 Element Plus的前后端分离架构。原因很简单预约挂号这类系统交互逻辑比较清晰前后端分离可以让后端接口保持RESTful风格同时前端组件化开发页面的复用和维护都方便。毕业设计和实际项目的评判重点在教学演示和功能完整性前端用CDN引入Vue和Element Plus就能跑不需要复杂的Node构建也降低了答辩演示时的环境风险。如果你的基础相对薄弱直接用SpringBoot的Thymeleaf模板引擎做服务端渲染也不是不行功能上一样能完成但Vue版本会让你的项目在答辩时的技术呈现更丰富一档。建议有条件的都上Vue。2.2 关键依赖清单与版本选择版本选择上我有一个血泪教训不要无脑追求最新版本。SpringBoot 3.x要求JDK 17并且javax.servlet换成jakarta.servlet一部分老教程的代码会直接报错。对于做课程设计和毕业设计的同学我的建议是用SpringBoot 2.7.x JDK 8或者JDK 11这是兼容性最稳的组合网上的资料、博客、Stack Overflow的答案基本都覆盖到这个版本遇到问题一搜就有答案。我本次项目的依赖组合如下组件版本说明JDK1.8 / 11不建议更高避免兼容性问题SpringBoot2.7.182.x系列的最后一个维护版本稳定性极佳MyBatis-Plus3.5.3极大简化单表CRUD操作MySQL5.7 / 8.0数据存储核心Redis可选本期项目未使用用数据库锁替代Sa-Token1.37.0轻量级权限认证框架knife4j4.x适配boot2.7接口文档自动生成Lombok最新即可减少样板代码提示如果服务器内存只有1G或2G就别装Redis了剩那点资源给MySQL和Java进程更实际。预约系统每天几百单的并发量数据库完全扛得住。2.3 分层架构与扩展性考量项目采用经典的四层架构Controller层接收HTTP请求参数校验调用Service层返回统一结果对象ResultT。Service层业务逻辑的核心载体比如创建预约的完整事务就在这里编排检查号源、锁定号源、创建预约记录、发送通知预留扩展点。Mapper层DAO层基于MyBatis-Plus单表CRUD全继承复杂查询用Select注解写SQL不单独建XML文件。实体层Model层数据库表对应的实体类加上DTO数据传输对象和VO视图对象做数据隔离。这套架构的好处是职责清晰答辩时被问到如果要做微服务改造怎么切入这类问题时你至少能说清楚哪些逻辑属于领域核心、哪些属于接口适配而不至于哑口无言。3. 核心设计与数据建模从中医诊所业务到表结构这节是全文的重头戏。我见过太多人一上来就撸代码结果做到一半发现表结构不合理业务根本绕不过去只好推倒重来。把表设计好整个项目就完成一半了。3.1 核心角色与业务边界梳理先对系统涉及的角色做一个清晰的梳理角色核心操作注意点患者普通用户注册登录、浏览医生排班、预约挂号、查询/取消预约核心诉求是快速找到要挂的大夫诊所管理员维护医生信息、配置排班、审核号源、查看统计数据核心诉求是可配置不用改代码中医师查看预约列表、维护患者病历、填写四诊信息核心诉求是看得到患者的历史情况系统管理员用户管理、角色分配、系统日志一般在课程设计中并入诊所管理员动作就那几个注册 → 选医生 → 看排班 → 预约 → 确认 →医生侧问诊 → 写病历。但业务边界要注意一个关键问题——患者是否必须注册才能挂号。有的系统做了访客挂号不登录也能约但中医诊所要求首诊留档基本属性决定了患者必须至少留下姓名、性别、联系方式这些基础信息否则医生没法做复诊跟踪。所以我的设计是必须注册登录才能预约但注册只需要手机号验证码测试环境可以做成固定验证码目的是降低使用门槛同时保留患者身份的唯一性。3.2 数据表设计详解下面是本项目的核心数据表结构设计我直接给出关键字段和设计理由。这不是网上随处抄的那套而是我结合实际业务考虑后的最终版本。表1用户表sys_user字段名类型说明idbigint主键雪花算法生成usernamevarchar(32)登录账号手机号passwordvarchar(64)BCrypt加密存储real_namevarchar(32)真实姓名phonevarchar(11)手机号gendertinyint性别user_typetinyint1-患者 2-中医师 3-管理员create_timedatetime创建时间表2中医师信息表doctor_info注意这里没有和sys_user合并原因在于登录用户和职业身份本质上是两种维度的数据一个用户未来可能既是患者又是医生虽然业务上不常见合并会导致字段冗余而且医生的职称、擅长领域、简介这类属性放用户表里非常不合适。字段名类型说明idbigint主键user_idbigint关联sys_user表titlevarchar(32)职称主任医师、副主任医师等specialtyvarchar(255)擅长领域introductiontext个人简介years_of_practiceint从业年限statustinyint1-出诊 0-停诊表3排班表schedule字段名类型说明idbigint主键doctor_idbigint关联医生work_datedate出诊日期start_timetime上午/下午时段开始时间end_timetime时段结束时间total_slotsint总号源数booked_slotsint已预约数statustinyint1-正常 0-已停诊这里的核心设计点是排班不是按时间点而是按时间段号源数。中医诊所的实际情况是大夫上午出诊半天患者来了按顺序看大约能看20到30个人没办法精确到9:30-9:45这个号。所以排班按半天做时间段用total_slots和booked_slots控制剩余量逻辑简单且符合诊所业务。表4预约记录表appointment字段名类型说明idbigint主键appointment_novarchar(32)预约编号业务编号如YY日期序号patient_idbigint患者用户IDdoctor_idbigint医生IDschedule_idbigint排班IDappointment_datedate预约日期time_slotvarchar(32)MORNING/AFTERNOONstatustinyint0-待就诊 1-已完成 2-已取消 3-爽约queue_noint排队序号同一天同一时段内递增create_timedatetime创建时间其中appointment_no为什么不直接用自增ID因为给患者看的预约凭证如果是纯数字自增ID容易暴露系统的预约总量也不够正规。用YY前缀日期序号生成业务单号观感专业而且可以通过单号快速定位预约记录实际开发中这是标配。表5病历表medical_record字段名类型说明idbigint主键patient_idbigint患者IDdoctor_idbigint接诊医生IDappointment_idbigint关联预约记录sympton_desctext主诉症状描述tonguevarchar(64)舌象pulsevarchar(64)脉象diagnosistext诊断辨证分型prescriptiontext处方内容create_timedatetime创建时间这张表是中医诊所和综合医院挂号系统的最大区别所在。综合医院的门诊病历是电子病历系统的职责和挂号系统是两个系统但在中医诊所的信息化方案里病历和预约必须打通。医生接诊时打开一个患者应该同时看到他以前在这家诊所的所有就诊记录和处方这样复诊的时候才能做前后对比。注意病历数据属于医疗敏感信息在你的课程设计文档和答辩PPT中一定要体现隐私保护和访问权限控制的意识这是加分项。后面我会专门讲权限控制怎么做。3.3 为什么不在表里直接存医生姓名做表设计的时候我坚持了一个原则所有关联字段全部存ID需要冗余展示字段时采用查询时组装而不是写入时冗余。比如appointment表里不存doctor_name和patient_name而是在查询预约列表时通过JOIN关联查询。这样做的理由有三个数据一致性。如果医生改了姓名换了个系统中的显示名历史预约记录里冗余的旧名字就成了脏数据。表结构更清晰业务边界明确各表各司其职。MyBatis-Plus支持TableField(exist false)标注非表字段配合关联查询组装数据并不算复杂。用一条LEFT JOIN就解决的事不值得用一堆冗余字段换。4. 预约流程的并发控制从乐观锁到唯一索引预约系统的核心难点在于同一时间很多人抢同一个号时如何保证数据不出错。这个场景要在课程设计里做出亮点不必上分布式锁但要能讲清楚你用的是什么方案以及为什么这个方案够用。4.1 问题的本质假设某个医生某天上午放出30个号现在只剩最后1个同时有两个患者点击预约。如果程序不做并发控制可能两个请求同时读到booked_slots29然后同时执行UPDATE schedule SET booked_slots30最后两个人都显示预约成功但实际数据变成了31甚至32。这就是典型的超卖问题和电商抢购是同一个逻辑。解决方案有很多种按实施复杂度从小到大排悲观锁SELECT ... FOR UPDATE写操作前就把这一行锁住其他事务只能等待。乐观锁版本号或条件更新更新时校验版本号不一致则更新失败。唯一索引在预约表上对(schedule_id, patient_id)建唯一索引同一患者对同一排班只能有一条有效预约。Redis分布式锁利用Redis的原子性实现互斥适合高并发场景。对于诊所预约系统这种每天几十到几百次预约请求的系统用方案23的组合已经完全够用而且逻辑简单易于解释。用Redis锁其实是杀鸡用牛刀还引入一个必须安装Redis的部署依赖不划算。4.2 乐观锁的具体实现MyBatis-Plus对乐观锁的支持非常友好。先在Schedule实体类的version字段上加上Version注解Version TableField(fill FieldFill.INSERT) private Integer version;然后在配置类中注册乐观锁插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }之后执行scheduleMapper.updateById(schedule)时MyBatis-Plus会自动在SQL里拼接WHERE version ?如果更新影响行数为0说明数据已经被别人修改过了操作失败。预约核心逻辑的Service层代码大致是这样的Transactional(rollbackFor Exception.class) public Result createAppointment(AppointmentCreateRequest request) { // 1. 获取排班信息 Schedule schedule scheduleMapper.selectById(request.getScheduleId()); if (schedule null) { return Result.error(排班不存在); } if (schedule.getStatus() ! 1) { return Result.error(该排班已停诊); } // 2. 检查是否已预约过唯一索引兜底这里做业务判断 LambdaQueryWrapperAppointment queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(Appointment::getScheduleId, request.getScheduleId()) .eq(Appointment::getPatientId, request.getPatientId()) .in(Appointment::getStatus, Arrays.asList(0, 1)); Long count appointmentMapper.selectCount(queryWrapper); if (count 0) { return Result.error(您已预约过该时段请勿重复预约); } // 3. 剩余号源判断 if (schedule.getBookedSlots() schedule.getTotalSlots()) { return Result.error(号源已约满); } // 4. 乐观锁扣减号源 schedule.setBookedSlots(schedule.getBookedSlots() 1); int updateRows scheduleMapper.updateById(schedule); if (updateRows 0) { return Result.error(号源已被抢完请刷新后重试); } // 5. 生成预约记录 Appointment appointment new Appointment(); appointment.setAppointmentNo(generateAppointmentNo(schedule.getWorkDate())); appointment.setPatientId(request.getPatientId()); appointment.setDoctorId(schedule.getDoctorId()); appointment.setScheduleId(schedule.getId()); appointment.setAppointmentDate(schedule.getWorkDate()); appointment.setTimeSlot(schedule.getTimeSlot()); appointment.setQueueNo(schedule.getBookedSlots()); // 排队号就是当前已预约数 appointment.setStatus(0); appointmentMapper.insert(appointment); return Result.success(appointment); }这里有几个设计细节需要特别说明Transactional保证了整个方法在同一事务中执行。如果第5步插入失败第4步的号源更新也会回滚不会出现号扣了但预约没建的数据不一致。乐观锁的判断归根到底靠的是updateById的返回值如果返回0就要主动抛出异常或返回失败不能继续往下走。排队序号queueNo直接取当前bookedSlots的值逻辑上是合理的——第几个预约成功就是第几号。这样患者看到的排队号是连续的观感也好。4.3 唯一索引兜底光靠乐观锁还有一个漏洞同一个患者的两个请求并发进来两者都通过了第2步的重复校验然后都走到第4步。这时候乐观锁会保证只有一个请求更新成功吗未必。因为两条更新语句对应的是同一行排班记录MyBatis-Plus的乐观锁是基于版本号的两个线程读到同样的版本号第一个更新成功第二个更新影响行数为0会被拦截。但这里前提是bookedSlots和version是同一行记录所以乐观锁本身是可以拦住超发的。不过为了做到系统级的兜底我还在appointment表上建了联合唯一索引ALTER TABLE appointment ADD UNIQUE KEY uk_schedule_patient (schedule_id, patient_id, status);等等这个索引有个问题status在运行中会从0变到1、变到2如果索引包含了status同一个患者取消预约后想再约同一个时段由于原记录status2已经存在新插入status0的记录时唯一索引会冲突。所以这个索引不能简单加。正确的做法是在业务代码里用状态为0或1的记录只能存在一条来保证而唯一索引只建在schedule_id和patient_id上也不合适因为取消后还想重约。所以在最终设计里我选择不建联合唯一索引而是依赖业务校验 乐观锁。理由很简单这个系统的并发量根本到不了需要用数据库索引兜底的量级而业务校验配合事务已经足够可靠。注意做课程设计的时候答辩老师很可能问到为什么这里不加唯一索引。你如果回答防止并发下重复预约那就要想清楚上面的问题。更好的答案是带着思辨地讲清楚——我评估了并发量级和业务重约场景最终用事务乐观锁来保证一致性。这比背一个标准答案更能体现工程思维。4.4 取消预约与号源释放有预约必然有取消。取消操作的逻辑是更新预约状态为已取消同时把排班的booked_slots减1。这里同样要知道一个并发细节取消操作和新增预约操作如果同时对同一个排班执行理论上也可能产生数据不一致。不过在事务层面两行UPDATE同一行记录数据库的行锁会保证它们的串行执行。也就是说先执行取消再执行新增或者反过来最终booked_slots都会维持在一个正确的值上。所以这里不需要额外加锁。取消预约代码Transactional(rollbackFor Exception.class) public Result cancelAppointment(Long appointmentId, Long userId) { Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null || !appointment.getPatientId().equals(userId)) { return Result.error(预约记录不存在); } if (appointment.getStatus() ! 0) { return Result.error(当前状态不可取消); } // 更新预约状态 appointment.setStatus(2); appointmentMapper.updateById(appointment); // 释放号源 Schedule schedule scheduleMapper.selectById(appointment.getScheduleId()); if (schedule ! null schedule.getBookedSlots() 0) { schedule.setBookedSlots(schedule.getBookedSlots() - 1); scheduleMapper.updateById(schedule); } return Result.success(); }这里有个小坑scheduleMapper.updateById(schedule)如果启用了乐观锁schedule对象是从数据库查到的最新的版本号是最新的所以更新不会失败。但如果查询之后隔了很久才做更新期间有其他人改了这行数据乐观锁就会导致取消失败。考虑到取消操作流程很短这个概率极低而且即使失败提示用户重试即可不影响整体体验。5. 权限控制与数据隔离从登录到三角色访问权限控制是管理系统的地基几乎是所有课程设计答辩的必问题。你这个系统患者能看到医生的界面吗答案必须是不能。Spring Security JWT或者Sa-Token都是可行方案。我选了Sa-Token理由后面会说。5.1 轻量级方案为什么弃用Spring SecuritySpring Security功能强大但学习曲线是真的陡。它的过滤器链、认证管理器、ProviderManager机制新手理解起来非常吃力。而Sa-Token的设计理念就是简单再简单一点——登录就是StpUtil.login(userId)获取当前登录用户就是StpUtil.getLoginId()权限校验就是SaCheckRole(admin)一个注解。对于中小型系统尤其是课程设计项目这种极简API带来的效率提升是立竿见影的。项目引入Sa-Token后sa-token: token-name: satoken timeout: 2592000 is-concurrent: true is-share: true token-style: uuid登录接口核心代码PostMapping(/login) public Result login(RequestBody LoginRequest request) { LambdaQueryWrapperSysUser wrapper new LambdaQueryWrapper(); wrapper.eq(SysUser::getUsername, request.getUsername()); SysUser user userMapper.selectOne(wrapper); if (user null || !BCrypt.passwordEncoder().matches(request.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (user.getStatus() ! 1) { return Result.error(账号已被禁用); } StpUtil.login(user.getId()); // 将用户角色信息写入会话 StpUtil.getSession().set(userType, user.getUserType()); // 构造登录返回数据 LoginResponse response new LoginResponse(); response.setToken(StpUtil.getTokenInfo().tokenValue); response.setRealName(user.getRealName()); response.setUserType(user.getUserType()); return Result.success(response); }5.2 基于角色的访问控制RBAC实现系统的角色分成三类患者、中医师、管理员。我采用了RBAC模型但在这个规模下不需要做角色-菜单-权限三级表直接在路由和接口层面做拦截后端拦截在WebMvcConfig中注册Sa-Token拦截器对/api/appointment/**、/api/medical-record/**等路径要求登录再用SaCheckRole注解控制角色范围。前端控制Vue Router中配置路由meta信息例如meta: { roles: [patient] }通过后端返回的用户角色动态生成可访问路由。后端接口的权限控制案例SaCheckRole(admin) PostMapping(/schedule) public Result createSchedule(RequestBody ScheduleRequest request) { // 管理员新建排班 } SaCheckRole(doctor) GetMapping(/appointment/list) public Result listAppointments(RequestParam Long scheduleId) { // 医生只能看自己的排班预约列表 }这里需要注意一个细节医生的数据无论如何都要做归属校验。比如医生查看预约列表时Service层要判断当前登录的医生ID和排班的doctorId是否一致。仅仅依赖前端隐藏按钮是不够的——用户完全可以绕过前端直接调API后端不校验就是越权漏洞。这是答辩时老师最喜欢深挖的一个点。5.3 病历数据的隔离与隐私保护病历表medical_record包含了患者的症状描述、舌苔脉象和处方信息属于高度敏感的医疗数据。在权限控制上我做了以下设计只有接诊医生本人和系统管理员能够查看患者的病历详情。患者本人可以查看自己的病历摘要但不能查看其他任何人的病历。在SQL查询层面强制拼接当前登录用户的ID作为过滤条件从根源上杜绝通过遍历ID查看他人病历的可能。病历读取逻辑SaCheckLogin GetMapping(/detail/{appointmentId}) public Result getRecordDetail(PathVariable Long appointmentId) { // 获取当前登录用户 long loginId StpUtil.getLoginIdAsLong(); MedicalRecord record medicalRecordMapper.getByAppointmentId(appointmentId); if (record null) { return Result.error(病历不存在); } // 数据归属校验 boolean isDoctor record.getDoctorId() loginId; boolean isPatient record.getPatientId() loginId; boolean isAdmin (Integer) StpUtil.getSession().get(userType) 3; if (!isDoctor !isPatient !isAdmin) { return Result.error(无权访问该病历); } return Result.success(record); }这套逻辑虽然朴实但涵盖了认证、授权、数据级权限三个层次答辩时按这个思路讲老师一般不会再追问太多。6. 管理后台的设计排班配置与数据统计管理后台往往是课程设计中最容易被忽视、但实际工作量最大的模块。患者端的预约流程做完管理端的排班管理、医生管理、统计报表还是得一个一个补。这一节说几个我认为值得花时间的点。6.1 排班配置页面管理端排班的核心痛点在于批量操作。比如要给张医生配置未来一个月每周一三五上午的出诊计划如果让管理员一天一条记录慢慢地填体验非常差也不符合实际诊所的使用场景。我的做法是在管理端提供一个**按周模板批量生成**的功能。配置内容包含选择医生选择星期周一、周三、周五选择时段上午8:00-12:00填入每个时段号源数比如上午30个选择起始日期和持续周数后端通过循环遍历日期批量生成schedule记录。同时做好两件事判断日期是否已经存在该医生的排班存在则跳过或者提示以及排班日期不能早于当前日期。6.2 预约统计的SQL实现管理后台需要展示基础的数据统计包括每天预约数量按日柱状图各医生的预约量排名用于了解哪些医生最受欢迎每个时段的上座率已预约数/总号源数这些都是经典的分组聚合查询用一条SQL就能解决。例如查询某时间段内各医生的预约人数排行SELECT d.real_name AS doctorName, COUNT(a.id) AS appointmentCount FROM appointment a LEFT JOIN doctor_info di ON a.doctor_id di.user_id LEFT JOIN sys_user d ON di.user_id d.id WHERE a.appointment_date BETWEEN #{startDate} AND #{endDate} AND a.status IN (0, 1) GROUP BY a.doctor_id ORDER BY appointmentCount DESC实现这些统计时我踩过一个坑由于appointment表的数据量不大直接用SQL统计完全没性能问题但我一开始因为想展现自己的后端能力引入了Spring Data Redis和缓存来存储统计数据结果不仅代码复杂度高数据还不实时缓存过期了要刷新反而把简单问题复杂化了。后来我直接把缓存层去掉导出报表或者前端图表的数据直接查库一次查询毫秒级返回根本不需要缓存。这个教训分享给大家先考虑直接查库性能真正有瓶颈了再上缓存不要为了用技术而用技术。6.3 前端图表的选型统计页面的图表目前主流方案是ECharts和AntV G2。ECharts生态更成熟用的人多文档和Demo也全面推荐直接用它。如果是Vue 3项目用vue-echarts封装或者直接在组件里引入都可以不复杂。7. 部署实录从jar包到远程服务器远程部署是整个项目交付的最后一个环节也是失误率最高的环节。很多人在本地跑得欢一到部署就连环踩坑。我把我实际部署过程踩过的问题列出来这些坑几乎每个做毕设的同学都会遇到。7.1 环境准备服务器上要装什么远程服务器我选的是LinuxCentOS 7或Ubuntu 20.04皆可1核2G内存的配置就够跑这个项目。服务器上需要安装的环境如下软件版本安装方式JDK1.8yum/apt 或上传tar包解压MySQL5.7/8.0在线安装或DockerNginx最新稳定版用于反向代理前端静态文件部署的整体流程是本地mvn clean package打包SpringBoot项目生成target/clinic-appointment.jar。将jar包通过scp命令或宝塔面板上传到服务器。在服务器上创建MySQL数据库导入初始化SQL脚本。修改配置文件的数据库连接地址为服务器的IP重新打包或通过外部配置文件覆盖。nohup java -jar clinic-appointment.jar --spring.profiles.activeprod 启动服务。前端项目npm run build后将dist目录上传到Nginx的html目录配置Nginx反代到后端8080端口。7.2 部署踩坑记录这里记录几个典型问题建议挨个对照。踩坑一打包后配置文件里的数据库连接还是本地地址这是新手最高频的问题。application.yml里写死了localhost:3306部署到服务器后连不上数据库。解决方案有两种一是打包前手动改成服务器的IP地址二是更优雅的方式——使用SpringBoot的Profile机制。application.yml里放公共配置application-dev.yml和application-prod.yml分别放不同环境的差异化配置启动时通过--spring.profiles.activeprod指定使用哪个Profile。这样本地开发和远程部署只需要启动命令不同代码不用改。踩坑二MySQL 8.0的驱动和连接配置变了MySQL 8.0的驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver同时需要在JDBC URL里追加时区参数url: jdbc:mysql://localhost:3306/clinic_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai不配serverTimezoneAsia/Shanghai启动时会报The server time zone value is unrecognized的异常。这个坑基本人人都会踩。踩坑三防火墙和云安全组没放行端口服务在服务器本机用curl localhost:8080测试是通的但外部访问却超时。排查思路先看服务是否启动成功ps -ef | grep java再看监听端口netstat -lnpt | grep 8080确认没问题后检查防火墙firewall-cmd --list-ports——如果用的是阿里云、腾讯云、华为云的服务器还要去控制台的安全组检查是否放行了8080端口。这个问题一般能卡住新手一整天。踩坑四Nginx配置反向代理时丢失了请求头前后端分离部署时前端请求/api路径要反代到后端http://localhost:8080但如果Nginx配置里少了proxy_set_header Host $host;这行后端获取的Host会是localhost:8080可能导致一些依赖Host的接口调用失败比如Sa-Token的Cookie写入、Swagger的接口地址生成。推荐的后端接口反代配置server { listen 80; server_name your-domain-or-ip; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }7.3 进程守护与日志排查线上服务不能一个java -jar裸跑终端一关服务就没了。推荐用nohup命令配合nohup java -jar clinic-appointment.jar --spring.profiles.activeprod logs/app.log 21 如果要进阶一点可以用systemd写一个service文件实现开机自启和崩溃自动重启。网上有很多模板这里就不展开了。遇到服务启动失败查看日志是最快的定位手段tail -200f logs/app.log看日志的典型思路是往回翻堆栈信息找到Caused by或APPLICATION FAILED TO START的部分对症处理。常见的原因无非就是数据库连接失败、端口被占用、配置的Redis连不上如果你用了、依赖的Bean不存在。8. 测试与交付模拟数据、演示要点与说明文档项目的代码写完不代表结束。对于毕业设计类的项目你还需要准备好演示用的数据、测试用例和一套能讲出来的完整叙述这在答辩中非常加分。8.1 造数据的艺术一个空荡荡的系统演示起来完全没有说服力。你需要提前准备一套覆盖常见业务场景的模拟数据4-5位中医师其中至少1位热门医师号源经常约满一位普通医师有空余号一位停诊医师。10-20个患者账号部分患者有历史预约记录和病历记录。未来两周的排班数据覆盖上午、下午不同时段。已有的预约记录中包含各种状态待就诊、已完成、已取消。这些数据直接通过Navicat手动插入也行写一个>
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门